'use strict';
// const ws = require('ws')
const util = require('util')
const events = require('events');
const { log, rtspWsStream } = require('../options.js')
const { spawn } = require('child_process')
let STREAM_MAGIC_BYTES = "jsmp" // Must be 4 bytes
class VideoStream {
    constructor(options) {
        this.options = options
        this.name = options.name || options.streamUrl;
        this.streamUrl = options.streamUrl
        this.width = options.width
        this.height = options.height
        this.wsPort = options.wsPort
        this.inputStreamStarted = false
        this.stream = undefined
        this.startNodeMediaServer()
        return this
    }

    stop() {
        this.stream && this.stream.kill()
        this.inputStreamStarted = false
        this.emit('stop')
        return this
    }
    md5(data) {
        let hash = require('crypto').createHash('md5');
        hash.update(data, 'utf8');
        return hash.digest('hex');
    }
    startNodeMediaServer() {
        if (this.inputStreamStarted) {
            return
        }
        this.additionalFlags = []
        if (rtspWsStream.ffmpegOptions) {
            for (let key in rtspWsStream.ffmpegOptions) {
                this.additionalFlags.push(key)
                if (String(rtspWsStream.ffmpegOptions[key]) !== '') {
                    this.additionalFlags.push(String(rtspWsStream.ffmpegOptions[key]))
                }
            }
        }
        this.output = `rtmp://localhost:${rtspWsStream.rtmp.port}/live/${this.md5(this.streamUrl)}`;
        let spawnOptions = [
            "-hide_banner",
            // 隐藏ffmpeg的版权信息
            "-re",
            // 以本地帧率读取输入流
            "-rtsp_transport",
            // rtsp传输协议
            "tcp",
            // tcp传输协议
            "-i", `${this.streamUrl}`,
            '-err_detect',
            // 忽略错误检测
            'ignore_err',
            // 忽略错误检测
            '-ignore_unknown',
            // 忽略未知流类型
            ...rtspWsStream.ffmpegOptions,
            `${this.output}`,
            "-probesize", rtspWsStream.probesize, // 设置输入缓存大小
            "-analyzeduration", "0", // 设置分析时间
            rtspWsStream.nostats ? "-nostats" : '', // 设置输入缓存大小
            rtspWsStream.loglevel ? "-loglevel" : '', rtspWsStream.loglevel || '', // 设置分析时间
        ]
        const ffmpegPath = rtspWsStream?.ffmpegPath || 'ffmpeg';
        log('ffmpeg', spawnOptions.join(' '), 'info')
        this.stream = spawn(ffmpegPath, spawnOptions, {
            detached: false
        })
        this.inputStreamStarted = true
        this.stream.stdout.on('data', (data) => {
            try {
                let str = data.toString()
                log('ffmpeg.stdout', str, 'debug')
            } catch(e) {}
            return this.emit('ffmpegData', data)
        })
        this.stream.stderr.on('data', (data) => {
            try {
                let str = data.toString()
                log('ffmpeg.stderr', str, 'debug')
            } catch(e) {}
            return this.emit('ffmpegStderr', data)
        })
        this.stream.on('exit', (code, signal) => {
            this.inputStreamStarted = false;
            this.exitCode = 1
            if (code === 255) {
                log('ffmpeg.release',`${this.streamUrl}, RTSP stream exited with code: ${code}`, 'info')
                return this.emit('exit')
            } else {
                log('socket.close',`${this.streamUrl}, RTSP stream exited with error code: ${code}`, 'error')
                return this.emit('exitWithError')
            }
        })
        return this
    }
}

util.inherits(VideoStream, events.EventEmitter)
/* ------------------------------------------------------------------
* node-onvif - device.js
*
* Copyright (c) 2016-2018, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2018-08-13
* ---------------------------------------------------------------- */
const mCrypto = require('crypto');
const mUrl = require('url');
const mUtil = require('util');
const mHtml = require('html');
const mEventEmitter = require('events').EventEmitter;
const mHttp = require('http');
const mHttps = require('https');
const mXml2Js = require('xml2js');
const mDgram = require('dgram');
const options = require('../options.js');

/* ------------------------------------------------------------------
* Constructor: OnvifSoap()
* ---------------------------------------------------------------- */
class OnvifSoap {
    constructor() {
        this.HTTP_TIMEOUT = 3000; // ms
    }
    /* ------------------------------------------------------------------
    * Method: parse(soap)
    * ---------------------------------------------------------------- */
    parse(soap) {
        let promise = new Promise((resolve, reject) => {
            let opts = {
                'explicitRoot': false,
                'explicitArray': false,
                'ignoreAttrs': false, // Never change to `true`
                'tagNameProcessors': [function (name) {
                    let m = name.match(/^([^\:]+)\:([^\:]+)$/);
                    return (m ? m[2] : name);
                }]
            };
            mXml2Js.parseString(soap, opts, (error, result) => {
                if (error) {
                    reject(error);
                } else {
                    resolve(result);
                }
            });
        });
        return promise;
    };

    /* ------------------------------------------------------------------
    * Method: requestCommand(oxaddr, method_name, soap)
    * ---------------------------------------------------------------- */
    requestCommand(oxaddr, method_name, soap) {
        let promise = new Promise((resolve, reject) => {
            let xml = '';
            this._request(oxaddr, soap).then((res) => {
                xml = res;
                return this.parse(xml);
            }).then((result) => {
                let fault = this._getFaultReason(result);
                if (fault) {
                    let err = new Error(fault);
                    reject(err);
                } else {
                    let parsed = this._parseResponseResult(method_name, result);
                    if (parsed) {
                        let res = {
                            'soap': xml,
                            'formatted': mHtml ? mHtml.prettyPrint(xml, { indent_size: 2 }) : '',
                            'converted': result,
                            'data': parsed
                        };
                        resolve(res);
                    } else {
                        let err = new Error('The device seems to not support the ' + method_name + '() method.');
                        reject(err);
                    }
                }
            }).catch((error) => {
                reject(error);
            });
        });
        return promise;
    };

    _parseResponseResult(method_name, res) {
        let s0 = res['Body'];
        if (!s0) { return null; }
        if ((method_name + 'Response') in s0) {
            return s0;
        } else {
            null;
        }
    };

    _request(oxaddr, soap) {
        let promise = new Promise((resolve, reject) => {
            let post_opts = {
                protocol: oxaddr.protocol,
                //auth    : oxaddr.auth,
                hostname: oxaddr.hostname,
                port: oxaddr.port || 80,
                path: oxaddr.pathname,
                method: 'POST',
                timeout: options.http.timeout || 5000,
                headers: {
                    //'Content-Type': 'application/soap+xml; charset=utf-8; action="http://www.onvif.org/ver10/device/wsdl/GetScopes"',
                    'Content-Type': 'application/soap+xml; charset=utf-8;',
                    'Content-Length': Buffer.byteLength(soap)
                }
            };

            let req = mHttp.request(post_opts, (res) => {
                res.setEncoding('utf8');
                let xml = '';
                res.on('data', (chunk) => {
                    xml += chunk;
                });
                res.on('end', () => {
                    if (req) {
                        req.removeAllListeners('error');
                        req.removeAllListeners('timeout');
                        req = null;
                    }
                    if (res) {
                        res.removeAllListeners('data');
                        res.removeAllListeners('end');
                    }
                    if (res.statusCode === 200) {
                        resolve(xml);
                    } else {
                        let err = new Error(res.statusCode + ' ' + res.statusMessage);
                        let code = res.statusCode;
                        let text = res.statusMessage;
                        if (xml) {
                            this.parse(xml).then((parsed) => {
                                let msg = '';
                                try {
                                    msg = parsed['Body']['Fault']['Reason']['Text'];
                                    if (typeof (msg) === 'object') {
                                        msg = msg['_'];
                                    }
                                } catch (e) { }
                                if (msg) {
                                    reject(new Error(code + ' ' + text + ' - ' + msg));
                                } else {
                                    reject(err);
                                }
                            }).catch((error) => {
                                reject(err);
                            });
                        } else {
                            reject(err);
                        }
                    }
                    res = null;
                });
            });

            req.setTimeout(this.HTTP_TIMEOUT);

            req.on('timeout', () => {
                req.abort();
            });

            req.on('error', (error) => {
                req.removeAllListeners('error');
                req.removeAllListeners('timeout');
                req = null;
                reject(new Error('Network Error: ' + (error ? error.message : '')));
            });

            req.write(soap, 'utf8');
            req.end();
        });
        return promise;
    };

    _getFaultReason(r) {
        let reason = '';
        try {
            let reason_el = r['Body']['Fault']['Reason'];
            if (reason_el['Text']) {
                reason = reason_el['Text'];
            } else {
                let code_el = r['Body']['Fault']['Code'];
                if (code_el['Value']) {
                    reason = code_el['Value'];
                    let subcode_el = code_el['Subcode'];
                    if (subcode_el['Value']) {
                        reason += ' ' + subcode_el['Value'];
                    }
                }
            }
        } catch (e) { }
        return reason;
    };

    /* ------------------------------------------------------------------
    * Method: createRequestSoap(params)
    * - params:
    *   - body: description in the <s:Body>
    *   - xmlns: a list of xmlns attributes used in the body
    *       e.g., xmlns:tds="http://www.onvif.org/ver10/device/wsdl"
    *   - diff: Time difference [ms]
    *   - user: user name
    *   - pass: password
    * ---------------------------------------------------------------- */
    createRequestSoap(params) {
        let soap = '';
        soap += '<?xml version="1.0" encoding="UTF-8"?>';
        soap += '<s:Envelope';
        soap += '  xmlns:s="http://www.w3.org/2003/05/soap-envelope"';
        if (params['xmlns'] && Array.isArray(params['xmlns'])) {
            params['xmlns'].forEach((ns) => {
                soap += ' ' + ns;
            });
        }
        soap += '>';
        soap += '<s:Header>';
        if (params['user']) {
            soap += this._createSoapUserToken(params['diff'], params['user'], params['pass']);
        }
        soap += '</s:Header>';
        soap += '<s:Body>' + params['body'] + '</s:Body>';
        soap += '</s:Envelope>';

        soap = soap.replace(/\>\s+\</g, '><');
        return soap;
    };

    _createSoapUserToken(diff, user, pass) {
        if (!diff) { diff = 0; }
        if (!pass) { pass = ''; }
        let date = (new Date(Date.now() + diff)).toISOString();
        let nonce_buffer = this._createNonce(16);
        let nonce_base64 = nonce_buffer.toString('base64');
        let shasum = mCrypto.createHash('sha1');
        shasum.update(Buffer.concat([nonce_buffer, new Buffer(date), new Buffer(pass)]));
        let digest = shasum.digest('base64');
        let soap = '';
        soap += '<Security s:mustUnderstand="1" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">';
        soap += '  <UsernameToken>';
        soap += '    <Username>' + user + '</Username>';
        soap += '    <Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">' + digest + '</Password>';
        soap += '    <Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">' + nonce_base64 + '</Nonce>';
        soap += '    <Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">' + date + '</Created>';
        soap += '  </UsernameToken>';
        soap += '</Security>';
        return soap;
    };

    _createNonce(digit) {
        let nonce = new Buffer(digit);
        for (let i = 0; i < digit; i++) {
            nonce.writeUInt8(Math.floor(Math.random() * 256), i);
        }
        return nonce;
    };

    /* ------------------------------------------------------------------
    * Method: isInvalidValue(value, type, allow_empty)
    * - type: 'undefined', 'null', 'array', 'integer', 'float', 'boolean', 'object'
    * ---------------------------------------------------------------- */
    isInvalidValue(value, type, allow_empty) {
        let vt = this._getTypeOfValue(value);
        if (type === 'float') {
            if (!vt.match(/^(float|integer)$/)) {
                return 'The type of the value must be "' + type + '".';
            }
        } else {
            if (vt !== type) {
                return 'The type of the value must be "' + type + '".';
            }
        }

        if (!allow_empty) {
            if (vt === 'array' && value.length === 0) {
                return 'The value must not be an empty array.';
            } else if (vt === 'string' && value === '') {
                return 'The value must not be an empty string.';
            }
        }
        if (typeof (value) === 'string') {
            if (value.match(/[^\x20-\x7e]/)) {
                return 'The value must consist of ascii characters.';
            }
            if (value.match(/[\<\>]/)) {
                return 'Invalid characters were found in the value ("<", ">")';
            }
        }
        return '';
    };

    _getTypeOfValue(value) {
        if (value === undefined) {
            return 'undefined';
        } else if (value === null) {
            return 'null';
        } else if (Array.isArray(value)) {
            return 'array';
        }
        let t = typeof (value);
        if (t === 'boolean') {
            return 'boolean';
        } else if (t === 'string') {
            return 'string';
        } else if (t === 'number') {
            if (value % 1 === 0) {
                return 'integer';
            } else {
                return 'float';
            }
        } else if (t === 'object') {
            if (Object.prototype.toString.call(value) === '[object Object]') {
                return 'object';
            } else {
                return 'unknown';
            }
        } else {
            return 'unknown';
        }
    }
}
/* ------------------------------------------------------------------
* node-onvif - service-device.js
*
* Copyright (c) 2016, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2016-10-02
* ---------------------------------------------------------------- */

/* ------------------------------------------------------------------
* Constructor: OnvifServiceDevice(params)
* - params:
*    - xaddr   : URL of the entry point for the device management service
*                (Required)
*    - user  : User name (Optional)
*    - pass  : Password (Optional)
* ---------------------------------------------------------------- */
class OnvifServiceDevice {
    constructor(params) {
        this.xaddr = '';
        this.user = '';
        this.pass = '';

        let err_msg = '';

        if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
            throw new Error('The value of "params" was invalid: ' + err_msg);
        }

        if ('xaddr' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['xaddr'], 'string')) {
                throw new Error('The "xaddr" property was invalid: ' + err_msg);
            } else {
                this.xaddr = params['xaddr'];
            }
        } else {
            throw new Error('The "xaddr" property is required.');
        }

        if ('user' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['user'], 'string', true)) {
                throw new Error('The "user" property was invalid: ' + err_msg);
            } else {
                this.user = params['user'] || '';
            }
        }

        if ('pass' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['pass'], 'string', true)) {
                throw new Error('The "pass" property was invalid: ' + err_msg);
            } else {
                this.pass = params['pass'] || '';
            }
        }

        this.oxaddr = mUrl.parse(this.xaddr);
        if (this.user) {
            this.oxaddr.auth = this.user + ':' + this.pass;
        }

        this.time_diff = 0;
        this.name_space_attr_list = [
            'xmlns:tds="http://www.onvif.org/ver10/device/wsdl"',
            'xmlns:tt="http://www.onvif.org/ver10/schema"'
        ];
    }

    _createRequestSoap(body) {
        let soap = mOnvifSoap.createRequestSoap({
            'body': body,
            'xmlns': this.name_space_attr_list,
            'diff': this.time_diff,
            'user': this.user,
            'pass': this.pass
        });
        return soap;
    };

    /* ------------------------------------------------------------------
    * Method: getTimeDiff()
    * ---------------------------------------------------------------- */
    getTimeDiff() {
        return this.time_diff;
    };

    /* ------------------------------------------------------------------
    * Method: setAuth(user, pass)
    * ---------------------------------------------------------------- */
    setAuth(user, pass) {
        this.user = user || '';
        this.pass = pass || '';
        if (this.user) {
            this.oxaddr.auth = this.user + ':' + this.pass;
        } else {
            this.oxaddr.auth = '';
        }
    };

    /* ------------------------------------------------------------------
    * Method: getCapabilities([callback])
    * ---------------------------------------------------------------- */
    getCapabilities(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '';
            soap_body += '<tds:GetCapabilities>';
            soap_body += '  <tds:Category>All</tds:Category>';
            soap_body += '</tds:GetCapabilities>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetCapabilities', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getWsdlUrl([callback])
    * ---------------------------------------------------------------- */
    getWsdlUrl(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetWsdlUrl/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetWsdlUrl', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getDiscoveryMode(callback)
    * ---------------------------------------------------------------- */
    getDiscoveryMode(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetDiscoveryMode/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetDiscoveryMode', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getScopes([callback])
    * ---------------------------------------------------------------- */
    getScopes(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetScopes/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetScopes', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setScopes(params[, callback])
    * - params:
    *   - Scopes  | Array  | required | a list of URI
    *
    * {
    *   'Scopes': [
    *     'onvif://www.onvif.org/location/town/Nerima',
    *     'onvif://www.onvif.org/location/city/Tokyo'
    *   ]
    * }
    *
    * If you want to delete all Configurable scopes, specify an empty
    * Array object as the `Scope` property.
    *
    * {'Scopes': []}
    * ---------------------------------------------------------------- */
    setScopes(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            let scope_list = params['Scopes'];
            if (err_msg = mOnvifSoap.isInvalidValue(scope_list, 'array', true)) {
                reject(new Error('The "Scopes" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tds:SetScopes>';
            scope_list.forEach((s) => {
                soap_body += '<tds:Scopes>' + s + '</tds:Scopes>';
            });
            soap_body += '</tds:SetScopes>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'SetScopes', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: addScopes(params[, callback])
    * - params:
    *   - Scopes  | Array  | required | a list of URI
    *
    * {
    *   'Scopes': [
    *     'onvif://www.onvif.org/location/town/Nerima',
    *     'onvif://www.onvif.org/location/city/Tokyo'
    *   ]
    * }
    * ---------------------------------------------------------------- */
    addScopes(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            let scope_list = params['Scopes'];
            if (err_msg = mOnvifSoap.isInvalidValue(scope_list, 'array', true)) {
                reject(new Error('The "Scopes" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tds:AddScopes>';
            scope_list.forEach((s) => {
                soap_body += '<tds:ScopeItem>' + s + '</tds:ScopeItem>';
            });
            soap_body += '</tds:AddScopes>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'AddScopes', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: removeScopes(params[, callback])
    * - params:
    *   - Scopes  | Array  | required | a list of URI
    *
    * {
    *   'Scopes': [
    *     'onvif://www.onvif.org/location/town/Nerima',
    *     'onvif://www.onvif.org/location/city/Tokyo'
    *   ]
    * }
    * ---------------------------------------------------------------- */
    removeScopes(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            let scope_list = params['Scopes'];
            if (err_msg = mOnvifSoap.isInvalidValue(scope_list, 'array')) {
                reject(new Error('The "Scopes" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tds:RemoveScopes>';
            scope_list.forEach((s) => {
                soap_body += '<tds:ScopeItem>' + s + '</tds:ScopeItem>';
            });
            soap_body += '</tds:RemoveScopes>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'RemoveScopes', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getHostname([callback])
    * ---------------------------------------------------------------- */
    getHostname(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetHostname/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetHostname', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setHostname(params[, callback])
    * - params:
    *   - Name  | string  | required | a host name
    *
    * {'Name': 'test'}
    * ---------------------------------------------------------------- */
    setHostname(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            let hostname = params['Name'];
            if (err_msg = mOnvifSoap.isInvalidValue(hostname, 'string')) {
                reject(new Error('The "Name" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tds:SetHostname>';
            soap_body += '<tds:Name>' + hostname + '</tds:Name>';
            soap_body += '</tds:SetHostname>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'SetHostname', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getDNS([callback])
    * ---------------------------------------------------------------- */
    getDNS(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetDNS/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetDNS', soap).then((result) => {
                try {
                    let di = result['data']['DNSInformation'];
                    if (!di['SearchDomain']) {
                        di['SearchDomain'] = [];
                    } else if (!Array.isArray(di['SearchDomain'])) {
                        di['SearchDomain'] = [di['SearchDomain']];
                    }
                    if (!di['DNSManual']) {
                        di['DNSManual'] = [];
                    } else if (!Array.isArray(di['DNSManual'])) {
                        di['DNSManual'] = [di['DNSManual']];
                    }
                    result['data'] = di;
                } catch (e) { }
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setDNS(params[, callback])
    * - params:
    *   - FromDHCP      | boolean | required | true or false
    *   - SearchDomain  | Array   | optional | a list of search domains
    *   - DNSManual     | Array   | optional | a list of DNS addresses
    *     - Type        | String  | required | "IPv4" or "IPv6"
    *     - IPv4Address | String  | optional | IPv4 address
    *     - IPv6Address | String  | optional | IPv6 address
    *
    * {
    *   'FromDHCP'    : false,
    *   'SearchDomain': ['futomi.gr.jp', 'hatano.gr.jp'],
    *   'DNSManual'   : [
    *     {'Type': 'IPv4', 'IPv4Address': '192.168.10.1'},
    *     {'Type': 'IPv4', 'IPv4Address': '192.168.10.2'}
    *   ]
    * }
    * ---------------------------------------------------------------- */
    setDNS(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['FromDHCP'], 'boolean')) {
                reject(new Error('The "FromDHCP" property was invalid: ' + err_msg));
                return;
            }

            if ('SearchDomain' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['SearchDomain'], 'array', true)) {
                    reject(new Error('The "SearchDomain" property was invalid: ' + err_msg));
                    return;
                }
                for (let i = 0; i < params['SearchDomain'].length; i++) {
                    if (err_msg = mOnvifSoap.isInvalidValue(params['SearchDomain'][i], 'string')) {
                        reject(new Error('The "SearchDomain" property was invalid: ' + err_msg));
                        return;
                    }
                }
            }

            if ('DNSManual' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['DNSManual'], 'array', true)) {
                    reject(new Error('The "DNSManual" property was invalid: ' + err_msg));
                    return;
                }

                for (let i = 0; i < params['DNSManual'].length; i++) {
                    let o = params['DNSManual'][i];
                    if (err_msg = mOnvifSoap.isInvalidValue(o, 'object')) {
                        reject(new Error('The "DNSManual" property was invalid: ' + err_msg));
                        return;
                    }

                    let type = o['Type'];
                    if (err_msg = mOnvifSoap.isInvalidValue(type, 'string')) {
                        reject(new Error('The "Type" property was invalid: ' + err_msg));
                        return;
                    } else if (!type.match(/^(IPv4|IPv6)$/)) {
                        reject(new Error('The "Type" property was invalid: The value must be either "IPv4" or "IPv6".'));
                        return;
                    }

                    if (type === 'IPv4') {
                        if (err_msg = mOnvifSoap.isInvalidValue(o['IPv4Address'], 'string')) {
                            reject(new Error('The "IPv4Address" property was invalid: ' + err_msg));
                            return;
                        }
                    } else if (type === 'IPv6') {
                        if (err_msg = mOnvifSoap.isInvalidValue(o['IPv6Address'], 'string')) {
                            reject(new Error('The "IPv6Address" property was invalid: ' + err_msg));
                            return;
                        }
                    }
                }
            }

            let soap_body = '';
            soap_body += '<tds:SetDNS>';
            if ('FromDHCP' in params) {
                soap_body += '<tds:FromDHCP>' + params['FromDHCP'] + '</tds:FromDHCP>';
            }
            if ('SearchDomain' in params) {
                params['SearchDomain'].forEach((s) => {
                    soap_body += '<tds:SearchDomain>' + s + '</tds:SearchDomain>';
                });
            }
            if ('DNSManual' in params) {
                if (params['DNSManual'].length === 0) {
                    soap_body += '<tds:DNSManual></tds:DNSManual>';
                } else {
                    params['DNSManual'].forEach((o) => {
                        soap_body += '<tds:DNSManual>';
                        soap_body += '<tt:Type>' + o['Type'] + '</tt:Type>';
                        if (o['Type'] === 'IPv4') {
                            soap_body += '<tt:IPv4Address>' + o['IPv4Address'] + '</tt:IPv4Address>';
                        } else {
                            soap_body += '<tt:IPv6Address>' + o['IPv6Address'] + '</tt:IPv6Address>';
                        }
                        soap_body += '</tds:DNSManual>';
                    });
                }
            }
            soap_body += '</tds:SetDNS>';

            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'SetDNS', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getNetworkInterfaces([callback])
    * ---------------------------------------------------------------- */
    getNetworkInterfaces(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetNetworkInterfaces/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetNetworkInterfaces', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getNetworkProtocols([callback])
    * ---------------------------------------------------------------- */
    getNetworkProtocols(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetNetworkProtocols/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetNetworkProtocols', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setNetworkProtocols(params[, callback])
    * - params:
    *   - NetworkProtocols | Array   | required | a list of protocols
    *     - Name           | String  | required |
    *     - Enabled        | Boolean | optional |
    *     - Port           | Integer | optional |
    *
    * {
    *   'NetworkProtocols': [
    *     {'Name': 'HTTP', 'Enabled': true, 'Port': 80},
    *     {'Name': 'RTSP', 'Enabled': false, 'Port': 554},
    *   ]
    * }
    *
    * ---------------------------------------------------------------- */
    setNetworkProtocols(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                callback(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['NetworkProtocols'], 'array', true)) {
                callback(new Error('The "NetworkProtocols" property was invalid: ' + err_msg));
                return;
            }

            for (let i = 0; i < params['NetworkProtocols'].length; i++) {
                let o = params['NetworkProtocols'][i];
                if (err_msg = mOnvifSoap.isInvalidValue(o, 'object')) {
                    callback(new Error('The "NetworkProtocols" property was invalid: ' + err_msg));
                    return;
                }

                if (err_msg = mOnvifSoap.isInvalidValue(o['Name'], 'string')) {
                    callback(new Error('The "Name" property was invalid: ' + err_msg));
                    return;
                } else if (!o['Name'].match(/^(HTTP|HTTPS|RTSP)$/)) {
                    callback(new Error('The "Name" property was invalid: It must be "HTTP", "HTTPS", or "RTSP".'));
                    return;
                }

                let flag = false;

                if ('Enabled' in o) {
                    if (err_msg = mOnvifSoap.isInvalidValue(o['Enabled'], 'boolean')) {
                        callback(new Error('The "Enabled" property was invalid: ' + err_msg));
                        return;
                    }
                    flag = true;
                }

                if ('Port' in o) {
                    if (err_msg = mOnvifSoap.isInvalidValue(o['Port'], 'integer')) {
                        callback(new Error('The "Port" property was invalid: ' + err_msg));
                        return;
                    }
                    flag = true;
                }

                if (flag === false) {
                    callbackError('Either "Enabled" or "Port" property is required.');
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<tds:SetNetworkProtocols>';
            params['NetworkProtocols'].forEach((o) => {
                soap_body += '<tds:NetworkProtocols>';
                for (let k in o) {
                    if (k.match(/^(Name|Enabled|Port)$/)) {
                        soap_body += '<tt:' + k + '>' + o[k] + '</tt:' + k + '>';
                    }
                }
                soap_body += '</tds:NetworkProtocols>';
            });
            soap_body += '</tds:SetNetworkProtocols>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'SetNetworkProtocols', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getNetworkDefaultGateway([callback])
    * ---------------------------------------------------------------- */
    getNetworkDefaultGateway(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetNetworkDefaultGateway/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetNetworkDefaultGateway', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setNetworkDefaultGateway(params[, callback])
    * - params:
    *   - NetworkGateway | Array | required | a list of IP addresses of gateways
    *
    * {
    *   'NetworkGateway': [
    *     {'IPv4Address': '192.168.10.1'},
    *     {'IPv4Address': '192.168.10.2'}
    *   ]
    * }
    *
    * ---------------------------------------------------------------- */
    setNetworkDefaultGateway(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['NetworkGateway'], 'array', true)) {
                reject(new Error('The "NetworkGateway" property was invalid: ' + err_msg));
                return;
            }

            for (let i = 0; i < params['NetworkGateway'].length; i++) {
                let o = params['NetworkGateway'][i];
                if ('IPv4Address' in o) {
                    if (err_msg = mOnvifSoap.isInvalidValue(o['IPv4Address'], 'string')) {
                        reject(new Error('The "IPv4Address" property was invalid: ' + err_msg));
                        return;
                    }
                }
                if ('IPv6Address' in o) {
                    if (err_msg = mOnvifSoap.isInvalidValue(o['IPv6Address'], 'string')) {
                        reject(new Error('The "IPv6Address" property was invalid: ' + err_msg));
                        return;
                    }
                }
                if (!('IPv4Address' in o) && !('IPv6Address' in o)) {
                    reject(new Error('Either "IPv4Address" or "IPv6Address" property must be specified.'));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<tds:SetNetworkDefaultGateway>';
            params['NetworkGateway'].forEach((o) => {
                for (let k in o) {
                    if (k.match(/^(IPv4Address|IPv6Address)$/)) {
                        soap_body += '<tds:' + k + '>' + o[k] + '</tds:' + k + '>';
                    }
                }
            });
            soap_body += '</tds:SetNetworkDefaultGateway>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'SetNetworkDefaultGateway', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getDeviceInformation([callback])
    * ---------------------------------------------------------------- */
    getDeviceInformation(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetDeviceInformation/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetDeviceInformation', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getSystemDateAndTime([callback])
    * ---------------------------------------------------------------- */
    getSystemDateAndTime(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetSystemDateAndTime/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetSystemDateAndTime', soap).then((result) => {
                let parsed = this._parseGetSystemDateAndTime(result['converted']);
                if (parsed && parsed['date']) {
                    let device_time = parsed['date'].getTime();
                    let my_time = (new Date()).getTime();
                    this.time_diff = device_time - my_time;
                }
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    _parseGetSystemDateAndTime(s) {
        let s0 = s['Body'];
        if (!s0) { return null; }
        let s1 = s0['GetSystemDateAndTimeResponse'];
        if (!s1) { return null; }
        let s2 = s1['SystemDateAndTime'];
        if (!s2) { return null; }

        let type = s2['DateTimeType'] || '';
        let dst = null;
        if (s2['DaylightSavings']) {
            dst = (s2['DaylightSavings'] === 'true') ? true : false;
        }
        let tz = (s2['TimeZone'] && s2['TimeZone']['TZ']) ? s2['TimeZone']['TZ'] : '';
        let date = null;
        if (s2['UTCDateTime']) {
            let udt = s2['UTCDateTime'];
            let t = udt['Time'];
            let d = udt['Date'];
            if (t && d && t['Hour'] && t['Minute'] && t['Second'] && d['Year'] && d['Month'] && d['Day']) {
                date = new Date();
                date.setUTCFullYear(parseInt(d['Year'], 10));
                date.setUTCMonth(parseInt(d['Month'], 10) - 1);
                date.setUTCDate(parseInt(d['Day'], 10));
                date.setUTCHours(parseInt(t['Hour'], 10));
                date.setUTCMinutes(parseInt(t['Minute'], 10));
                date.setUTCSeconds(parseInt(t['Second'], 10));
            }
        }
        let res = {
            'type': type,
            'dst': dst,
            'tz': tz,
            'date': date
        };
        return res;
    };

    /* ------------------------------------------------------------------
    * Method: setSystemDateAndTime(params[, callback])
    * - params:
    *   - DateTimeType    | string  | required | "NTP" or "Manual".
    *   - DaylightSavings | boolean | required | true or false.
    *   - TimeZone        | string  | optional | e.g., "EST5EDT", "GMT0", "JST-9".
    *   - UTCDateTime     | Date    | optional | A Date object of ECMAScript.
    *
    * Setting the "UTCDateTime" does not work well for now.
    * ---------------------------------------------------------------- */
    setSystemDateAndTime(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['DateTimeType'], 'string')) {
                reject(new Error('The "DateTimeType" property was invalid: ' + err_msg));
                return;
            } else if (!params['DateTimeType'].match(/^(NTP|Manual)$/)) {
                reject(new Error('The "DateTimeType" property must be either "NTP" or "Manual".'));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['DaylightSavings'], 'boolean')) {
                reject(new Error('The "DaylightSavings" property was invalid: ' + err_msg));
                return;
            }

            if ('TimeZone' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['TimeZone'], 'string')) {
                    reject(new Error('The "TimeZone" property was invalid: ' + err_msg));
                    return;
                } else if (!params['TimeZone'].match(/^[A-Z]{3}\-?\d{1,2}([A-Z]{3,4})?$/)) {
                    reject(new Error('The "TimeZone" property must be a string representing a time zone which is defined in POSIX 1003.1.'));
                    return;
                }
            }

            if ('UTCDateTime' in params) {
                let v = params['UTCDateTime'];
                if (!v instanceof Date) {
                    reject(new Error('The "UTCDateTime" property must be a Date object of ECMAScript.'));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<tds:SetSystemDateAndTime>';
            soap_body += '<tds:DateTimeType>' + params['DateTimeType'] + '</tds:DateTimeType>';
            soap_body += '<tds:DaylightSavings>' + params['DaylightSavings'] + '</tds:DaylightSavings>';
            if (params['TimeZone']) {
                soap_body += '<tds:TimeZone>';
                soap_body += '<tt:TZ>' + params['TimeZone'] + '</tt:TZ>';
                soap_body += '</tds:TimeZone>';
            }
            if (params['UTCDateTime']) {
                let dt = params['UTCDateTime'];
                soap_body += '<tds:UTCDateTime>';
                soap_body += '<tt:Time>';
                soap_body += '<tt:Hour>' + dt.getUTCHours() + '</tt:Hour>';
                soap_body += '<tt:Minute>' + dt.getUTCMinutes() + '</tt:Minute>';
                soap_body += '<tt:Second>' + dt.getUTCSeconds() + '</tt:Second>';
                soap_body += '</tt:Time>';
                soap_body += '<tt:Date>';
                soap_body += '<tt:Year>' + dt.getUTCFullYear() + '</tt:Year>';
                soap_body += '<tt:Month>' + (dt.getUTCMonth() + 1) + '</tt:Month>';
                soap_body += '<tt:Day>' + dt.getUTCDate() + '</tt:Day>';
                soap_body += '</tt:Date>';
                soap_body += '</tds:UTCDateTime>';
            }
            soap_body += '</tds:SetSystemDateAndTime>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'SetSystemDateAndTime', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: reboot([callback])
    * ---------------------------------------------------------------- */
    reboot(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:SystemReboot/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'SystemReboot', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getUsers([callback])
    * ---------------------------------------------------------------- */
    getUsers(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetUsers/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetUsers', soap).then((result) => {
                try {
                    let d = result['data']['GetUsersResponse']['User'];
                    if (!Array.isArray(d)) {
                        result['data']['GetUsersResponse']['User'] = [d];
                    }
                } catch (e) { }
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: createUsers(params[, callback])
    * - params:
    *   - User        | Array  | required |
    *     - Username  | string | required | Username
    *     - Password  | string | required | Password
    *     - UserLevel | string | required | Either "Administrator", "Operator", "User", or "Anonymous"
    *
    * {
    *   'User' : [
    *     {'Username': 'test1', 'Password' : 'password', 'UserLevel': 'Administrator'},
    *     {'Username': 'test2', 'Password' : 'password', 'UserLevel': 'Operator'},
    *     {'Username': 'test3', 'Password' : 'password', 'UserLevel': 'User'}
    *   ]
    * }
    * ---------------------------------------------------------------- */
    createUsers(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['User'], 'array')) {
                reject(new Error('The "User" property was invalid: ' + err_msg));
                return;
            }

            for (let i = 0; i < params['User'].length; i++) {
                let u = params['User'][i];
                if (err_msg = mOnvifSoap.isInvalidValue(u, 'object')) {
                    reject(new Error('The "User" property was invalid: ' + err_msg));
                    return;
                }

                if (err_msg = mOnvifSoap.isInvalidValue(u['Username'], 'string')) {
                    reject(new Error('The "Username" property was invalid: ' + err_msg));
                    return;
                }

                if (err_msg = mOnvifSoap.isInvalidValue(u['Password'], 'string')) {
                    reject(new Error('The "Password" property was invalid: ' + err_msg));
                    return;
                }

                if (err_msg = mOnvifSoap.isInvalidValue(u['UserLevel'], 'string')) {
                    reject(new Error('The "UserLevel" property was invalid: ' + err_msg));
                    return;
                } else if (!u['UserLevel'].match(/^(Administrator|Operator|User|Anonymous)$/)) {
                    reject(new Error('The "UserLevel" property must be either "Administrator", "Operator", "User", or "Anonymous".'));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<tds:CreateUsers>';
            params['User'].forEach((u) => {
                soap_body += '<tds:User>';
                soap_body += '<tt:Username>' + u['Username'] + '</tt:Username>';
                soap_body += '<tt:Password>' + u['Password'] + '</tt:Password>';
                soap_body += '<tt:UserLevel>' + u['UserLevel'] + '</tt:UserLevel>';
                soap_body += '</tds:User>';
            });
            soap_body += '</tds:CreateUsers>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'CreateUsers', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: deleteUsers(params[, callback])
    * - params:
    *   - User        | Array  | required |
    *     - Username  | string | required | Username
    *
    * {
    *   'User' : [
    *     {'Username': 'test1'},
    *     {'Username': 'test2'},
    *     {'Username': 'test3'}
    *   ]
    * }
    * ---------------------------------------------------------------- */
    deleteUsers(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['User'], 'array')) {
                reject(new Error('The "User" property was invalid: ' + err_msg));
                return;
            }

            for (let i = 0; i < params['User'].length; i++) {
                let u = params['User'][i];
                if (err_msg = mOnvifSoap.isInvalidValue(u, 'object')) {
                    reject(new Error('The "User" property was invalid: ' + err_msg));
                    return;
                }

                if (err_msg = mOnvifSoap.isInvalidValue(u['Username'], 'string')) {
                    reject(new Error('The "Username" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<tds:DeleteUsers>';
            params['User'].forEach((u) => {
                //soap_body += '<tds:User>';
                soap_body += '<tt:Username>' + u['Username'] + '</tt:Username>';
                //soap_body += '</tds:User>';
            });
            soap_body += '</tds:DeleteUsers>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'DeleteUsers', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setUser(params[, callback])
    * - params:
    *   - User        | Array  | required |
    *     - Username  | string | required | Username
    *     - Password  | string | optional | Password
    *     - UserLevel | string | optional | Either "Administrator", "Operator", "User", or "Anonymous"
    *
    * {
    *   'User' : [
    *     {'Username': 'test1', 'Password' : 'password', 'UserLevel': 'Administrator'},
    *     {'Username': 'test2', 'Password' : 'password', 'UserLevel': 'Operator'},
    *     {'Username': 'test3', 'Password' : 'password', 'UserLevel': 'User'}
    *   ]
    * }
    * ---------------------------------------------------------------- */
    setUser(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['User'], 'array')) {
                reject(new Error('The "User" property was invalid: ' + err_msg));
                return;
            }

            for (let i = 0; i < params['User'].length; i++) {
                let u = params['User'][i];
                if (err_msg = mOnvifSoap.isInvalidValue(u, 'object')) {
                    reject(new Error('The "User" property was invalid: ' + err_msg));
                    return;
                }

                if (err_msg = mOnvifSoap.isInvalidValue(u['Username'], 'string')) {
                    reject(new Error('The "Username" property was invalid: ' + err_msg));
                    return;
                }

                if ('Password' in u) {
                    if (err_msg = mOnvifSoap.isInvalidValue(u['Password'], 'string')) {
                        reject(new Error('The "Password" property was invalid: ' + err_msg));
                        return;
                    }
                }

                if ('UserLevel' in u) {
                    if (err_msg = mOnvifSoap.isInvalidValue(u['UserLevel'], 'string')) {
                        reject(new Error('The "UserLevel" property was invalid: ' + err_msg));
                        return;
                    } else if (!u['UserLevel'].match(/^(Administrator|Operator|User|Anonymous)$/)) {
                        reject(new Error('The "UserLevel" property must be either "Administrator", "Operator", "User", or "Anonymous".'));
                        return;
                    }
                }
            }

            let soap_body = '';
            soap_body += '<tds:SetUser>';
            params['User'].forEach((u) => {
                soap_body += '<tds:User>';
                soap_body += '<tt:Username>' + u['Username'] + '</tt:Username>';
                if ('Password' in u) {
                    soap_body += '<tt:Password>' + u['Password'] + '</tt:Password>';
                }
                if ('UserLevel' in u) {
                    soap_body += '<tt:UserLevel>' + u['UserLevel'] + '</tt:UserLevel>';
                }
                soap_body += '</tds:User>';
            });
            soap_body += '</tds:SetUser>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'SetUser', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getRelayOutputs([callback])
    * ---------------------------------------------------------------- */
    getRelayOutputs(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetRelayOutputs/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetRelayOutputs', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getNTP([callback])
    * ---------------------------------------------------------------- */
    getNTP(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetNTP/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetNTP', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setNTP(params[, callback])
    * - params:
    *   - FromDHCP      | Boolean | required | true or false
    *   - NTPManual     | Object  | optional |
    *     - Type        | String  | required | "IPv4" or "IPv6"
    *     - IPv4Address | String  | required | IP address
    *
    * {
    *    "FromDHCP": "false",
    *    "NTPManual": {"Type": "IPv4", "IPv4Address": "192.168.10.1"}
    * }
    * ---------------------------------------------------------------- */
    setNTP(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['FromDHCP'], 'boolean')) {
                reject(new Error('The "FromDHCP" property was invalid: ' + err_msg));
                return;
            }

            if ('NTPManual' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['NTPManual'], 'object')) {
                    reject(new Error('The "NTPManual" property was invalid: ' + err_msg));
                    return;
                }

                let o = params['NTPManual'];

                let type = o['Type'];
                if (err_msg = mOnvifSoap.isInvalidValue(type, 'string')) {
                    reject(new Error('The "Type" property was invalid: ' + err_msg));
                    return;
                } else if (!type.match(/^(IPv4|IPv6)$/)) {
                    reject(new Error('The "Type" property was invalid: The value must be either "IPv4" or "IPv6".'));
                    return;
                }

                if (type === 'IPv4') {
                    if (err_msg = mOnvifSoap.isInvalidValue(o['IPv4Address'], 'string')) {
                        reject(new Error('The "IPv4Address" property was invalid: ' + err_msg));
                        return;
                    }
                } else if (type === 'IPv6') {
                    if (err_msg = mOnvifSoap.isInvalidValue(o['IPv6Address'], 'string')) {
                        reject(new Error('The "IPv6Address" property was invalid: ' + err_msg));
                        return;
                    }
                }
            }

            let soap_body = '';
            soap_body += '<tds:SetNTP>';
            soap_body += '<tds:FromDHCP>' + params['FromDHCP'] + '</tds:FromDHCP>';
            if ('NTPManual' in params) {
                let o = params['NTPManual'];
                soap_body += '<tds:NTPManual>';
                soap_body += '<tt:Type>' + o['Type'] + '</tt:Type>';
                if (o['Type'] === 'IPv4') {
                    soap_body += '<tt:IPv4Address>' + o['IPv4Address'] + '</tt:IPv4Address>';
                } else {
                    soap_body += '<tt:IPv6Address>' + o['IPv6Address'] + '</tt:IPv6Address>';
                }
                soap_body += '</tds:NTPManual>';
            }
            soap_body += '</tds:SetNTP>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'SetNTP', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getDynamicDNS([callback])
    * ---------------------------------------------------------------- */
    getDynamicDNS(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetDynamicDNS/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetDynamicDNS', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getZeroConfiguration([callback])
    * ---------------------------------------------------------------- */
    getZeroConfiguration(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetZeroConfiguration/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetZeroConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getIPAddressFilter([callback])
    * No devcie I own supports this method for now.
    * ---------------------------------------------------------------- */
    getIPAddressFilter(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetIPAddressFilter/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetIPAddressFilter', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setIPAddressFilter(params[, callback])
    * - params:
    *   - Type:          | String  | required | 'Allow', 'Deny' 
    *   - IPv4Address    | Array   | required | 
    *     - Address      | String  | required | IPv4 address
    *     - PrefixLength | Integer | required | Prefix/submask length
    *
    * {
    *    "Type": "Allow",
    *    "IPv4Address": [
    *      {
    *        "Address": "192.168.10.3",
    *        "PrefixLength": 24
    *      }
    *    ]
    * ]}
    *
    * No devcie I own supports this method for now.
    * ---------------------------------------------------------------- */
    setIPAddressFilter(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['Type'], 'string')) {
                reject(new Error('The "Type" property was invalid: ' + err_msg));
                return;
            } else if (!params['Type'].match(/^(Allow|Deny)$/)) {
                reject(new Error('The "Type" property was invalid: The value must be either "Allow" or "Deny".'));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['IPv4Address'], 'array')) {
                reject(new Error('The "IPv4Address" property was invalid: ' + err_msg));
                return;
            }

            for (let i = 0; i < params['IPv4Address'].length; i++) {
                let o = params['IPv4Address'][i];
                if (err_msg = mOnvifSoap.isInvalidValue(o, 'object')) {
                    reject(new Error('The "IPv4Address" property was invalid: ' + err_msg));
                    return;
                }
                if (err_msg = mOnvifSoap.isInvalidValue(o['Address'], 'string')) {
                    reject(new Error('The "Address" property was invalid: ' + err_msg));
                    return;
                } else if (!o['Address'].match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
                    reject(new Error('The "Address" property was invalid as a IPv4 address.'));
                    return;
                }
                if (err_msg = mOnvifSoap.isInvalidValue(o['PrefixLength'], 'integer')) {
                    reject(new Error('The "PrefixLength" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<tds:SetIPAddressFilter>';
            soap_body += '<tds:IPAddressFilter>';
            soap_body += '<tt:Type>' + params['Type'] + '</tt:Type>';
            params['IPv4Address'].forEach((o) => {
                soap_body += '<tt:IPv4Address>';
                soap_body += '<tt:Address>' + o['Address'] + '</tt:Address>';
                soap_body += '<tt:PrefixLength>' + o['PrefixLength'] + '</tt:PrefixLength>';
                soap_body += '</tt:IPv4Address>';
            });
            soap_body += '</tds:IPAddressFilter>';
            soap_body += '</tds:SetIPAddressFilter>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'SetIPAddressFilter', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getServices(params[, callback])
    * - params:
    *   - IncludeCapability | boolean | required | true or false
    *
    * {'IncludeCapability': false}
    * ---------------------------------------------------------------- */
    getServices(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['IncludeCapability'], 'boolean')) {
                reject(new Error('The "IncludeCapability" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tds:GetServices>';
            soap_body += '<tds:IncludeCapability>' + params['IncludeCapability'] + '</tds:IncludeCapability>';
            soap_body += '</tds:GetServices>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetServices', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getServiceCapabilities([callback])
    * ---------------------------------------------------------------- */
    getServiceCapabilities(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tds:GetServiceCapabilities/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetServiceCapabilities', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };
}
/* ------------------------------------------------------------------
* node-onvif - service-media.js
*
* Copyright (c) 2016 - 2017, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2017-08-26
* ---------------------------------------------------------------- */

/* ------------------------------------------------------------------
* Constructor: OnvifServiceMedia(params)
* - params:
*    - xaddr   : URL of the entry point for the media service
*                (Required)
*    - user  : User name (Optional)
*    - pass  : Password (Optional)
*    - time_diff: ms
* ---------------------------------------------------------------- */
class OnvifServiceMedia {
    constructor(params) {
        this.xaddr = '';
        this.user = '';
        this.pass = '';

        let err_msg = '';

        if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
            throw new Error('The value of "params" was invalid: ' + err_msg);
        }

        if ('xaddr' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['xaddr'], 'string')) {
                throw new Error('The "xaddr" property was invalid: ' + err_msg);
            } else {
                this.xaddr = params['xaddr'];
            }
        } else {
            throw new Error('The "xaddr" property is required.');
        }

        if ('user' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['user'], 'string', true)) {
                throw new Error('The "user" property was invalid: ' + err_msg);
            } else {
                this.user = params['user'] || '';
            }
        }

        if ('pass' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['pass'], 'string', true)) {
                throw new Error('The "pass" property was invalid: ' + err_msg);
            } else {
                this.pass = params['pass'] || '';
            }
        }

        this.oxaddr = mUrl.parse(this.xaddr);
        if (this.user) {
            this.oxaddr.auth = this.user + ':' + this.pass;
        }

        this.time_diff = params['time_diff'];
        this.name_space_attr_list = [
            'xmlns:trt="http://www.onvif.org/ver10/media/wsdl"',
            'xmlns:tt="http://www.onvif.org/ver10/schema"'
        ];
    }

    _createRequestSoap(body) {
        let soap = mOnvifSoap.createRequestSoap({
            'body': body,
            'xmlns': this.name_space_attr_list,
            'diff': this.time_diff,
            'user': this.user,
            'pass': this.pass
        });
        return soap;
    };

    /* ------------------------------------------------------------------
    * Method: setAuth(user, pass)
    * ---------------------------------------------------------------- */
    setAuth(user, pass) {
        this.user = user || '';
        this.pass = pass || '';
        if (this.user) {
            this.oxaddr.auth = this.user + ':' + this.pass;
        } else {
            this.oxaddr.auth = '';
        }
    };

    /* ------------------------------------------------------------------
    * Method: getStreamUri(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the profile
    *   - Protocol     | String | required | "UDP", "HTTP", or "RTSP"
    *
    * {
    *   'ProfileToken': 'Profile1,
    *   'Protocol'    : 'UDP'
    * }
    * ---------------------------------------------------------------- */
    getStreamUri(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['Protocol'], 'string')) {
                reject(new Error('The "Protocol" property was invalid: ' + err_msg));
                return;
            } else if (!params['Protocol'].match(/^(UDP|HTTP|RTSP)$/)) {
                reject(new Error('The "Protocol" property was invalid: The value must be either "UDP", "HTTP", or "RTSP".'));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetStreamUri>';
            soap_body += '<trt:StreamSetup>';
            soap_body += '<tt:Stream>RTP-Unicast</tt:Stream>';
            soap_body += '<tt:Transport>';
            soap_body += '<tt:Protocol>' + params['Protocol'] + '</tt:Protocol>';
            soap_body += '</tt:Transport>';
            soap_body += '</trt:StreamSetup>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:GetStreamUri>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetStreamUri', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getVideoEncoderConfigurations([callback])
    * ---------------------------------------------------------------- */
    getVideoEncoderConfigurations(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '';
            soap_body += '<trt:GetVideoEncoderConfigurations />';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetVideoEncoderConfigurations', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getVideoEncoderConfiguration(params[, callback])
    * - params:
    *   - ConfigurationToken | String | required | a token of the configuration
    *
    * {
    *   'ConfigurationToken': 'Configuration1'
    * }
    * ---------------------------------------------------------------- */
    getVideoEncoderConfiguration(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetVideoEncoderConfiguration>';
            soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            soap_body += '</trt:GetVideoEncoderConfiguration>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetVideoEncoderConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getCompatibleVideoEncoderConfigurations(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the profile
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getCompatibleVideoEncoderConfigurations(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetCompatibleVideoEncoderConfigurations>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:GetCompatibleVideoEncoderConfigurations>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetCompatibleVideoEncoderConfigurations', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getVideoEncoderConfigurationOptions(params[, callback])
    * - params:
    *   - ProfileToken       | String | optional | a token of the profile
    *   - ConfigurationToken | String | optional | a token of the configuration
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getVideoEncoderConfigurationOptions(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if ('ProfileToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                    reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            if ('ConfigurationToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                    reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<trt:GetVideoEncoderConfigurationOptions>';
            if (params['ProfileToken']) {
                soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            }
            if (params['ConfigurationToken']) {
                soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            }
            soap_body += '</trt:GetVideoEncoderConfigurationOptions>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetVideoEncoderConfigurationOptions', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getGuaranteedNumberOfVideoEncoderInstances(params[, callback])
    * - params:
    *   - ConfigurationToken | String | required | a token of the configuration
    *
    * {
    *   'ConfigurationToken': 'Configuration1'
    * }
    * ---------------------------------------------------------------- */
    getGuaranteedNumberOfVideoEncoderInstances(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetGuaranteedNumberOfVideoEncoderInstances>';
            soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            soap_body += '</trt:GetGuaranteedNumberOfVideoEncoderInstances>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetGuaranteedNumberOfVideoEncoderInstances', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getProfiles([callback])
    * ---------------------------------------------------------------- */
    getProfiles(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<trt:GetProfiles/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetProfiles', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getProfile(params[, callback])
    * - params:
    *   - ProfileToken | required | a token of the profile
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getProfile(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetProfile>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:GetProfile>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetProfile', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: createProfile(params[, callback])
    * - params:
    *   - Name  | String | required | a name of the profile
    *   - Token | String | optional | a token of the profile
    *
    * {
    *   'Name: 'TestProfile1'
    * }
    * ---------------------------------------------------------------- */
    createProfile(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['Name'], 'string')) {
                reject(new Error('The "Name" property was invalid: ' + err_msg));
                return;
            }

            if ('Token' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['Token'], 'string')) {
                    reject(new Error('The "Token" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<trt:CreateProfile>';
            soap_body += '<trt:Name>' + params['Name'] + '</trt:Name>';
            if ('Token' in params) {
                soap_body += '<trt:Token>' + params['Token'] + '</trt:Token>';
            }
            soap_body += '</trt:CreateProfile>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'CreateProfile', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: deleteProfile(params[, callback])
    * - params:
    *   - ProfileToken | String | required | 
    *
    * {
    *   'ProfileToken: 'TestProfile1'
    * }
    * ---------------------------------------------------------------- */
    deleteProfile(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:DeleteProfile>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:DeleteProfile>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'DeleteProfile', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getVideoSources([callback])
    * ---------------------------------------------------------------- */
    getVideoSources(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<trt:GetVideoSources/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetVideoSources', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getVideoSourceConfiguration(params[, callback])
    * - params:
    *   - ConfigurationToken | String | required | 
    *
    * {
    *   'ConfigurationToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getVideoSourceConfiguration(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetVideoSourceConfiguration>';
            soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            soap_body += '</trt:GetVideoSourceConfiguration>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetVideoSourceConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getVideoSourceConfigurations([callback])
    * ---------------------------------------------------------------- */
    getVideoSourceConfigurations(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<trt:GetVideoSourceConfigurations/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetVideoSourceConfigurations', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: addVideoSourceConfiguration(params[, callback])
    * - params:
    *   - ProfileToken       | String | required | a token of the Profile
    *   - ConfigurationToken | String | required | 
    *
    * {
    *   'ProfileToken': 'Profile1'
    *   'ConfigurationToken': 'Profile1'
    * }
    *
    * No device I own does not support this command
    * ---------------------------------------------------------------- */
    addVideoSourceConfiguration(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:AddVideoSourceConfiguration>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            soap_body += '</trt:AddVideoSourceConfiguration>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'AddVideoSourceConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getCompatibleVideoSourceConfigurations(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the targeted PTZ node
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getCompatibleVideoSourceConfigurations(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetCompatibleVideoSourceConfigurations>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:GetCompatibleVideoSourceConfigurations>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetCompatibleVideoSourceConfigurations', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getVideoSourceConfigurationOptions(params[, callback])
    * - params:
    *   - ProfileToken       | optional | a token of the Profile
    *   - ConfigurationToken | optional | a token of the configuration
    *
    * {
    *   'ProfileToken': 'Profile1'
    *   'ConfigurationToken': 'Conf1'
    * }
    * ---------------------------------------------------------------- */
    getVideoSourceConfigurationOptions(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if ('ProfileToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                    reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            if ('ConfigurationToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                    reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<trt:GetVideoSourceConfigurationOptions>';
            if ('ProfileToken' in params) {
                soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            }
            if ('ConfigurationToken' in params) {
                soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            }
            soap_body += '</trt:GetVideoSourceConfigurationOptions>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetVideoSourceConfigurationOptions', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getMetadataConfiguration(params[, callback])
    * - params:
    *   - ConfigurationToken | required | 
    *
    * {
    *   'ConfigurationToken': 'Conf1'
    * }
    * ---------------------------------------------------------------- */
    getMetadataConfiguration(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetMetadataConfiguration>';
            soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            soap_body += '</trt:GetMetadataConfiguration>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetMetadataConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getMetadataConfigurations([callback])
    * ---------------------------------------------------------------- */
    getMetadataConfigurations(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<trt:GetMetadataConfigurations/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetMetadataConfigurations', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: addMetadataConfiguration(params[, callback])
    * - params:
    *   - ProfileToken       | String | required | a token of the Profile
    *   - ConfigurationToken | String | required | 
    *
    * {
    *   'ProfileToken': 'Profile1'
    *   'ConfigurationToken': 'Conf1'
    * }
    *
    * No device I own does not support this command
    * ---------------------------------------------------------------- */
    addMetadataConfiguration(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:AddMetadataConfiguration>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            soap_body += '</trt:AddMetadataConfiguration>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'AddMetadataConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getCompatibleMetadataConfigurations(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the Profile
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getCompatibleMetadataConfigurations(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetCompatibleMetadataConfigurations>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:GetCompatibleMetadataConfigurations>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetCompatibleMetadataConfigurations', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getMetadataConfigurationOptions(params[, callback])
    * - params:
    *   - ProfileToken       | String | optional | a token of the Profile
    *   - ConfigurationToken | String | optional | 
    *
    * {
    *   'ProfileToken': 'Profile1'
    *   'ConfigurationToken': 'Conf1'
    * }
    * ---------------------------------------------------------------- */
    getMetadataConfigurationOptions(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if ('ProfileToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                    reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            if ('ConfigurationToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                    reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<trt:GetMetadataConfigurationOptions>';
            if ('ProfileToken' in params) {
                soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            }
            if ('ConfigurationToken' in params) {
                soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            }
            soap_body += '</trt:GetMetadataConfigurationOptions>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetMetadataConfigurationOptions', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getAudioSources([callback])
    * ---------------------------------------------------------------- */
    getAudioSources(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<trt:GetAudioSources/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetAudioSources', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getAudioSourceConfiguration(params[, callback])
    * - params:
    *   - ConfigurationToken | String | required | 
    *
    * {
    *   'ConfigurationToken': 'Conf1'
    * }
    * ---------------------------------------------------------------- */
    getAudioSourceConfiguration(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetAudioSourceConfiguration>';
            soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            soap_body += '</trt:GetAudioSourceConfiguration>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetAudioSourceConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getAudioSourceConfigurations([callback])
    * ---------------------------------------------------------------- */
    getAudioSourceConfigurations(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<trt:GetAudioSourceConfigurations/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetAudioSourceConfigurations', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: addAudioSourceConfiguration(params[, callback])
    * - params:
    *   - ProfileToken       | String | required | a token of the Profile
    *   - ConfigurationToken | String | required |  
    *
    * {
    *   'ProfileToken': 'Profile1',
    *   'ConfigurationToken': 'Conf1'
    * }
    *
    * No device I own does not support this command
    * ---------------------------------------------------------------- */
    addAudioSourceConfiguration(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:AddAudioSourceConfiguration>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            soap_body += '</trt:AddAudioSourceConfiguration>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'AddAudioSourceConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getCompatibleAudioSourceConfigurations(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the profile
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getCompatibleAudioSourceConfigurations(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetCompatibleAudioSourceConfigurations>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:GetCompatibleAudioSourceConfigurations>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetCompatibleAudioSourceConfigurations', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getAudioSourceConfigurationOptions(params[, callback])
    * - params:
    *   - ProfileToken       | String | optional | a token of the Profile
    *   - ConfigurationToken | String | optional | 
    *
    * {
    *   'ProfileToken': 'Profile1'
    *   'ConfigurationToken': 'Conf1'
    * }
    * ---------------------------------------------------------------- */
    getAudioSourceConfigurationOptions(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if ('ProfileToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                    reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            if ('ConfigurationToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                    reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<trt:GetAudioSourceConfigurationOptions>';
            if ('ProfileToken' in params) {
                soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            }
            if ('ConfigurationToken' in params) {
                soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            }
            soap_body += '</trt:GetAudioSourceConfigurationOptions>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetAudioSourceConfigurationOptions', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getAudioEncoderConfiguration(params[, callback])
    * - params:
    *   - ConfigurationToken | String | required | 
    *
    * {
    *   'ConfigurationToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getAudioEncoderConfiguration(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetAudioEncoderConfiguration>';
            soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            soap_body += '</trt:GetAudioEncoderConfiguration>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetAudioEncoderConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getAudioEncoderConfigurations([callback])
    * ---------------------------------------------------------------- */
    getAudioEncoderConfigurations(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<trt:GetAudioEncoderConfigurations/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetAudioEncoderConfigurations', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: addAudioEncoderConfiguration(params[, callback])
    * - params:
    *   - ProfileToken       | String | required | a token of the Profile
    *   - ConfigurationToken | String | required |  
    *
    * {
    *   'ProfileToken': 'Profile1',
    *   'ConfigurationToken': 'Conf1'
    * }
    *
    * Not device I own does not support this command
    * ---------------------------------------------------------------- */
    addAudioEncoderConfiguration(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:AddAudioEncoderConfiguration>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            soap_body += '</trt:AddAudioEncoderConfiguration>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'AddAudioEncoderConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getCompatibleAudioEncoderConfigurations(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the profile
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getCompatibleAudioEncoderConfigurations(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetCompatibleAudioEncoderConfigurations>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:GetCompatibleAudioEncoderConfigurations>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetCompatibleAudioEncoderConfigurations', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getAudioEncoderConfigurationOptions(params[, callback])
    * - params:
    *   - ProfileToken       | String | optional | a token of the Profile
    *   - ConfigurationToken | String | optional | 
    *
    * {
    *   'ProfileToken': 'Profile1'
    *   'ConfigurationToken': 'Conf1'
    * }
    * ---------------------------------------------------------------- */
    getAudioEncoderConfigurationOptions(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if ('ProfileToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                    reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            if ('ConfigurationToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                    reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<trt:GetAudioEncoderConfigurationOptions>';
            if ('ProfileToken' in params) {
                soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            }
            if ('ConfigurationToken' in params) {
                soap_body += '<trt:ConfigurationToken>' + params['ConfigurationToken'] + '</trt:ConfigurationToken>';
            }
            soap_body += '</trt:GetAudioEncoderConfigurationOptions>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetAudioEncoderConfigurationOptions', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: startMulticastStreaming(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the Profile
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    *
    * No device I own does not support this command
    * ---------------------------------------------------------------- */
    startMulticastStreaming(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:StartMulticastStreaming>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:StartMulticastStreaming>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'StartMulticastStreaming', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: stopMulticastStreaming(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the Profile
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    *
    * No device I own does not support this command
    * ---------------------------------------------------------------- */
    stopMulticastStreaming(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:StopMulticastStreaming>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:StopMulticastStreaming>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'StopMulticastStreaming', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getSnapshotUri(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the Profile
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getSnapshotUri(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<trt:GetSnapshotUri>';
            soap_body += '<trt:ProfileToken>' + params['ProfileToken'] + '</trt:ProfileToken>';
            soap_body += '</trt:GetSnapshotUri>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetSnapshotUri', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };
};
/* ------------------------------------------------------------------
* node-onvif - service-ptz.js
*
* Copyright (c) 2016 - 2017, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2017-08-30
* ---------------------------------------------------------------- */

/* ------------------------------------------------------------------
* Constructor: OnvifServicePtz(params)
* - params:
*    - xaddr   : URL of the entry point for the media service
*                (Required)
*    - user  : User name (Optional)
*    - pass  : Password (Optional)
*    - time_diff: ms
* ---------------------------------------------------------------- */
class OnvifServicePtz {
    constructor(params) {
        this.xaddr = '';
        this.user = '';
        this.pass = '';

        let err_msg = '';

        if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
            throw new Error('The value of "params" was invalid: ' + err_msg);
        }

        if ('xaddr' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['xaddr'], 'string')) {
                throw new Error('The "xaddr" property was invalid: ' + err_msg);
            } else {
                this.xaddr = params['xaddr'];
            }
        } else {
            throw new Error('The "xaddr" property is required.');
        }

        if ('user' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['user'], 'string', true)) {
                throw new Error('The "user" property was invalid: ' + err_msg);
            } else {
                this.user = params['user'] || '';
            }
        }

        if ('pass' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['pass'], 'string', true)) {
                throw new Error('The "pass" property was invalid: ' + err_msg);
            } else {
                this.pass = params['pass'] || '';
            }
        }

        this.oxaddr = mUrl.parse(this.xaddr);
        if (this.user) {
            this.oxaddr.auth = this.user + ':' + this.pass;
        }

        this.time_diff = params['time_diff'];
        this.name_space_attr_list = [
            'xmlns:ter="http://www.onvif.org/ver10/error"',
            'xmlns:xs="http://www.w3.org/2001/XMLSchema"',
            'xmlns:tt="http://www.onvif.org/ver10/schema"',
            'xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl"'
        ];
    }

    _createRequestSoap(body) {
        let soap = mOnvifSoap.createRequestSoap({
            'body': body,
            'xmlns': this.name_space_attr_list,
            'diff': this.time_diff,
            'user': this.user,
            'pass': this.pass
        });
        return soap;
    };

    /* ------------------------------------------------------------------
    * Method: setAuth(user, pass)
    * ---------------------------------------------------------------- */
    setAuth(user, pass) {
        this.user = user || '';
        this.pass = pass || '';
        if (this.user) {
            this.oxaddr.auth = this.user + ':' + this.pass;
        } else {
            this.oxaddr.auth = '';
        }
    };

    /* ------------------------------------------------------------------
    * Method: getNodes([callback])
    * ---------------------------------------------------------------- */
    getNodes(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tptz:GetNodes/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetNodes', soap).then((result) => {
                try {
                    let d = result['data']['PTZNode'];
                    if (!Array.isArray(d)) {
                        result['data']['PTZNode'] = [d];
                    }
                } catch (e) { }
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getNode(params[, callback])
    * - params:
    *   - NodeToken | String | required | a token of the targeted PTZ node
    * ---------------------------------------------------------------- */
    getNode(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['NodeToken'], 'string')) {
                reject(new Error('The "NodeToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tptz:GetNode>';
            soap_body += '<tptz:NodeToken>' + params['NodeToken'] + '</tptz:NodeToken>';
            soap_body += '</tptz:GetNode>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetNode', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getConfigurations([callback])
    * ---------------------------------------------------------------- */
    getConfigurations(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '<tptz:GetConfigurations/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetConfigurations', soap).then((result) => {
                try {
                    let d = result['data']['PTZConfiguration'];
                    if (!Array.isArray(d)) {
                        result['data']['PTZConfiguration'] = [d];
                    }
                } catch (e) { }
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };


    /* ------------------------------------------------------------------
    * Method: getConfiguration(params[, callback])
    * - params:
    *   - ConfigurationToken | String | required | a token of the targeted PTZ node
    *
    * No device I own does not work well for now.
    * ---------------------------------------------------------------- */
    getConfiguration(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }
            let soap_body = '';
            soap_body += '<tptz:GetConfiguration>';
            soap_body += '<tptz:PTZConfigurationToken>' + params['ConfigurationToken'] + '</tptz:PTZConfigurationToken>';
            soap_body += '</tptz:GetConfiguration>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetConfiguration', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getConfigurationOptions(params[, callback])
    * - params:
    *   - ConfigurationToken | String | required | a token of the targeted PTZ node
    * ---------------------------------------------------------------- */
    getConfigurationOptions(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ConfigurationToken'], 'string')) {
                reject(new Error('The "ConfigurationToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tptz:GetConfigurationOptions>';
            soap_body += '<tptz:ConfigurationToken>' + params['ConfigurationToken'] + '</tptz:ConfigurationToken>';
            soap_body += '</tptz:GetConfigurationOptions>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetConfigurationOptions', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getStatus(params[, callback])
    * - params:
    *   - ProfileToken | String | required |
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getStatus(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tptz:GetStatus>';
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';
            soap_body += '</tptz:GetStatus>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetStatus', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: continuousMove(params[, callback]) 持续转动镜头
    * - params:
    *   - ProfileToken | String  | required | 
    *   - Velocity     | Object  | required | pan, tilt and zoom
    *     - x          | Float   | required |
    *     - y          | Float   | required |
    *     - z          | Float   | required |
    *   - Timeout      | Integer | optional | timeout (seconds)
    *
    * {
    *   'ProfileToken': 'Profile1',
    *   'Velocity': {'x': 0.5, 'y': 1.0, 'z': 1.0},
    *   'Timeout': 5
    * }
    * ---------------------------------------------------------------- */
    continuousMove(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['Velocity'], 'object')) {
                reject(new Error('The "Velocity" property was invalid: ' + err_msg));
                return;
            }

            let klist = ['x', 'y', 'z'];
            for (let i = 0; i < klist.length; i++) {
                let k = klist[i];
                let v = params['Velocity'][k];
                if (err_msg = mOnvifSoap.isInvalidValue(v, 'float')) {
                    reject(new Error('The "' + k + '" property was invalid: ' + err_msg));
                    return;
                }
            }

            if ('Timeout' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['Timeout'], 'integer')) {
                    reject(new Error('The "Timeout" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<tptz:ContinuousMove>';
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';
            soap_body += '<tptz:Velocity>';
            soap_body += '<tt:PanTilt x="' + params['Velocity']['x'] + '" y="' + params['Velocity']['y'] + '"></tt:PanTilt>';
            if (params['Velocity']['z']) {
                soap_body += '<tt:Zoom x="' + params['Velocity']['z'] + '"></tt:Zoom>';
            }
            soap_body += '</tptz:Velocity>';
            if (params['Timeout']) {
                soap_body += '<tptz:Timeout>PT' + params['Timeout'] + 'S</tptz:Timeout>';
            }
            soap_body += '</tptz:ContinuousMove>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'ContinuousMove', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: absoluteMove(params[, callback])
    * - params:
    *   - ProfileToken | String  | required |
    *   - Position     | Object  | required | pan, tilt and zoom
    *     - x          | Float   | required |
    *     - y          | Float   | required |
    *     - z          | Float   | required |
    *   - Speed        | Object  | optional | pan, tilt and zoom
    *     - x          | Float   | required |
    *     - y          | Float   | required |
    *     - z          | Float   | required |
    *
    * {
    *   'ProfileToken': cam['ProfileToken'],
    *   'Position'    : {'x': 0.5, 'y': -1, 'z': 0.5},
    *   'Speed'       : {'x': 1, 'y': 1, 'z': 1}
    * }
    * ---------------------------------------------------------------- */
    absoluteMove(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['Position'], 'object')) {
                reject(new Error('The "Position" property was invalid: ' + err_msg));
                return;
            }

            let klist = ['x', 'y', 'z'];

            for (let i = 0; i < klist.length; i++) {
                let k = klist[i];
                let v = params['Position'][k];
                if (err_msg = mOnvifSoap.isInvalidValue(v, 'float')) {
                    reject(new Error('The "' + k + '" property was invalid: ' + err_msg));
                    return;
                }
            }

            if ('Speed' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['Speed'], 'object')) {
                    reject(new Error('The "Speed" property was invalid: ' + err_msg));
                    return;
                }
                for (let i = 0; i < klist.length; i++) {
                    let k = klist[i];
                    let v = params['Speed'][k];
                    if (err_msg = mOnvifSoap.isInvalidValue(v, 'float')) {
                        reject(new Error('The "' + k + '" property was invalid: ' + err_msg));
                        return;
                    }
                }
            }

            let soap_body = '';
            soap_body += '<tptz:AbsoluteMove>';
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';

            soap_body += '<tptz:Position>';
            soap_body += '<tt:PanTilt x="' + params['Position']['x'] + '" y="' + params['Position']['y'] + '" />';
            soap_body += '<tt:Zoom x="' + params['Position']['z'] + '"/>';
            soap_body += '</tptz:Position>';

            if (params['Speed']) {
                soap_body += '<tptz:Speed>';
                soap_body += '<tt:PanTilt x="' + params['Speed']['x'] + '" y="' + params['Speed']['y'] + '" />';
                soap_body += '<tt:Zoom x="' + params['Speed']['z'] + '"/>';
                soap_body += '</tptz:Speed>';
            }

            soap_body += '</tptz:AbsoluteMove>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'AbsoluteMove', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: relativeMove(params[, callback])
    * - params:
    *   - ProfileToken | String  | required | 
    *   - Translation  | Object  | required | pan, tilt and zoom
    *     - x          | Float   | required |
    *     - y          | Float   | required |
    *     - z          | Float   | required |
    *   - Speed        | Object  | optional | pan, tilt and zoom
    *     - x          | Float   | required |
    *     - y          | Float   | required |
    *     - z          | Float   | required |
    *
    * {
    *   'ProfileToken': 'Profile1',
    *   'Translation' : {'x': 0.5, 'y': 1.0, 'z': 1.0},
    *   'Speed'       : {'x': 1, 'y': 1, 'z': 1}
    * }
    * ---------------------------------------------------------------- */
    relativeMove(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['Translation'], 'object')) {
                reject(new Error('The "Translation" property was invalid: ' + err_msg));
                return;
            }

            let klist = ['x', 'y', 'z'];
            for (let i = 0; i < klist.length; i++) {
                let k = klist[i];
                let v = params['Translation'][k];
                if (err_msg = mOnvifSoap.isInvalidValue(v, 'float')) {
                    reject(new Error('The "' + k + '" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<tptz:RelativeMove>';
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';
            soap_body += '<tptz:Translation>';
            soap_body += '<tt:PanTilt x="' + params['Translation']['x'] + '" y="' + params['Translation']['y'] + '"/>';
            soap_body += '<tt:Zoom x="' + params['Translation']['z'] + '"/>';
            soap_body += '</tptz:Translation>';
            soap_body += '</tptz:RelativeMove>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'RelativeMove', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: stop(params[, callback])
    * - params:
    *   - ProfileToken | String  | required | a token of the targeted PTZ node
    *   - PanTilt      | Boolean | optional | true or false
    *   - Zoom         | Boolean | optional | true or false
    *
    * {
    *   'ProfileToken': 'Profile1',
    *   'PanTilt': true,
    *   'Zoom': true
    * }
    * ---------------------------------------------------------------- */
    stop(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if ('PanTilt' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['PanTilt'], 'boolean')) {
                    reject(new Error('The "PanTilt" property was invalid: ' + err_msg));
                    return;
                }
            }

            if ('Zoom' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['Zoom'], 'boolean')) {
                    reject(new Error('The "Zoom" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<tptz:Stop>';
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';
            if ('PanTilt' in params) {
                soap_body += '<tptz:PanTilt>' + params['PanTilt'] + '</tptz:PanTilt>';
            }
            if ('Zoom' in params) {
                soap_body += '<tptz:Zoom>' + params['Zoom'] + '</tptz:Zoom>';
            }
            soap_body += '</tptz:Stop>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'Stop', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: gotoHomePosition(params[, callback])
    * - params:
    *   - ProfileToken | String | required |
    *   - Speed        | Float  | optional |
    *
    * {
    *   'ProfileToken': 'Profile1',
    *   'Speed': 0.5
    * }
    * ---------------------------------------------------------------- */
    gotoHomePosition(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if ('Speed' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['Speed'], 'float')) {
                    reject(new Error('The "Speed" property was invalid: ' + err_msg));
                    return;
                }
            }

            let soap_body = '';
            soap_body += '<tptz:GotoHomePosition>';
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';
            if ('Speed' in params) {
                soap_body += '<tptz:Speed>' + params['Speed'] + '</tptz:Speed>';
            }
            soap_body += '</tptz:GotoHomePosition>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GotoHomePosition', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setHomePosition(params[, callback])
    * - params:
    *   - ProfileToken | String | required |
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    setHomePosition(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tptz:SetHomePosition>';
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';
            soap_body += '</tptz:SetHomePosition>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'SetHomePosition', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setPreset(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the targeted PTZ node
    *   - PresetToken  | String | optional |
    *   - PresetName   | String | optional |
    *
    * {
    *   'ProfileToken': 'Profile1',
    *   'PresetName'  : 'Preset1'
    * }
    * ---------------------------------------------------------------- */
    setPreset(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if ('PresetToken' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['PresetToken'], 'string')) {
                    reject(new Error('The "PresetToken" property was invalid: ' + err_msg));
                    return;
                }
            }

            if ('PresetName' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['PresetName'], 'string')) {
                    reject(new Error('The "PresetName" property was invalid: ' + err_msg));
                    return;
                }
            }

            if (!('PresetToken' in params) && !('PresetName' in params)) {
                reject('Either the "ProfileToken" or the "PresetName" property must be specified.');
                return;
            }

            let cmdType = 'SetPreset'; // params['isadd'] === 111 ? 'AddPreset' : 'SetPreset'
            let soap_body = '';
            soap_body += `<tptz:${cmdType}>`;
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';
            if ('PresetToken' in params) {
                soap_body += '<tptz:PresetToken>' + params['PresetToken'] + '</tptz:PresetToken>';
            }
            if ('PresetName' in params) {
                soap_body += '<tptz:PresetName>' + params['PresetName'] + '</tptz:PresetName>';
            }
            soap_body += `</tptz:${cmdType}>`;
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, cmdType, soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getPresets(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the targeted PTZ node
    *
    * {
    *   'ProfileToken': 'Profile1'
    * }
    * ---------------------------------------------------------------- */
    getPresets(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tptz:GetPresets>';
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';
            soap_body += '</tptz:GetPresets>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GetPresets', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: gotoPreset(params[, callback])
    * - params:
    *   - ProfileToken | String  | required | 
    *   - PresetToken  | String  | required | 
    *   - Speed        | Object  | optional | pan, tilt and zoom
    *     - x          | Float   | required |
    *     - y          | Float   | required |
    *     - z          | Float   | required |
    *
    * {
    *   'ProfileToken': 'Profile1',
    *   'PresetToken' : 'Preset1',
    *   'Speed'       : {'x': 0.5, 'y': 1.0, 'z': 0.5}
    * }
    * ---------------------------------------------------------------- */
    gotoPreset(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['PresetToken'], 'string')) {
                reject(new Error('The "PresetToken" property was invalid: ' + err_msg));
                return;
            }

            let klist = ['x', 'y', 'z'];
            if ('Speed' in params) {
                if (err_msg = mOnvifSoap.isInvalidValue(params['Speed'], 'object')) {
                    reject(new Error('The "Speed" property was invalid: ' + err_msg));
                    return;
                }
                for (let i = 0; i < klist.length; i++) {
                    let k = klist[i];
                    let v = params['Speed'][k];
                    if (err_msg = mOnvifSoap.isInvalidValue(v, 'float')) {
                        reject(new Error('The "' + k + '" property was invalid: ' + err_msg));
                        return;
                    }
                }
            }

            let soap_body = '';
            soap_body += '<tptz:GotoPreset>';
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';
            soap_body += '<tptz:PresetToken>' + params['PresetToken'] + '</tptz:PresetToken>';
            if (params['Speed']) {
                soap_body += '<tptz:Speed>';
                soap_body += '<tt:PanTilt x="' + params['Speed']['x'] + '" y="' + params['Speed']['y'] + '" />';
                soap_body += '<tt:Zoom x="' + params['Speed']['z'] + '"/>';
                soap_body += '</tptz:Speed>';
            }
            soap_body += '</tptz:GotoPreset>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'GotoPreset', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: removePreset(params[, callback])
    * - params:
    *   - ProfileToken | String | required | a token of the targeted PTZ node
    *   - PresetToken  | String | required |
    *
    * {
    *   'ProfileToken': 'Profile1',
    *   'PresetToken' : 'Preset1'
    * }
    * ---------------------------------------------------------------- */
    removePreset(params, callback) {
        let promise = new Promise((resolve, reject) => {
            let err_msg = '';
            if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
                reject(new Error('The value of "params" was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['ProfileToken'], 'string')) {
                reject(new Error('The "ProfileToken" property was invalid: ' + err_msg));
                return;
            }

            if (err_msg = mOnvifSoap.isInvalidValue(params['PresetToken'], 'string')) {
                reject(new Error('The "PresetToken" property was invalid: ' + err_msg));
                return;
            }

            let soap_body = '';
            soap_body += '<tptz:RemovePreset>';
            soap_body += '<tptz:ProfileToken>' + params['ProfileToken'] + '</tptz:ProfileToken>';
            soap_body += '<tptz:PresetToken>' + params['PresetToken'] + '</tptz:PresetToken>';
            soap_body += '</tptz:RemovePreset>';
            let soap = this._createRequestSoap(soap_body);

            mOnvifSoap.requestCommand(this.oxaddr, 'RemovePreset', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };
}
/* ------------------------------------------------------------------
* node-onvif - service-events.js
*
* Copyright (c) 2016 - 2017, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2017-08-26
* ---------------------------------------------------------------- */

/* ------------------------------------------------------------------
* Constructor: OnvifServiceEvents(params)
* - params:
*    - xaddr   : URL of the entry point for the media service
*                (Required)
*    - user  : User name (Optional)
*    - pass  : Password (Optional)
*    - time_diff: ms
* ---------------------------------------------------------------- */
class OnvifServiceEvents {
    constructor(params) {
        this.xaddr = '';
        this.user = '';
        this.pass = '';

        let err_msg = '';

        if (err_msg = mOnvifSoap.isInvalidValue(params, 'object')) {
            throw new Error('The value of "params" was invalid: ' + err_msg);
        }

        if ('xaddr' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['xaddr'], 'string')) {
                throw new Error('The "xaddr" property was invalid: ' + err_msg);
            } else {
                this.xaddr = params['xaddr'];
            }
        } else {
            throw new Error('The "xaddr" property is required.');
        }

        if ('user' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['user'], 'string', true)) {
                throw new Error('The "user" property was invalid: ' + err_msg);
            } else {
                this.user = params['user'] || '';
            }
        }

        if ('pass' in params) {
            if (err_msg = mOnvifSoap.isInvalidValue(params['pass'], 'string', true)) {
                throw new Error('The "pass" property was invalid: ' + err_msg);
            } else {
                this.pass = params['pass'] || '';
            }
        }

        this.oxaddr = mUrl.parse(this.xaddr);
        if (this.user) {
            this.oxaddr.auth = this.user + ':' + this.pass;
        }

        this.time_diff = params['time_diff'];
        this.name_space_attr_list = [
            'xmlns:wsa="http://www.w3.org/2005/08/addressing"',
            'xmlns:tev="http://www.onvif.org/ver10/events/wsdl"'
        ];
    }

    _createRequestSoap(body) {
        let soap = mOnvifSoap.createRequestSoap({
            'body': body,
            'xmlns': this.name_space_attr_list,
            'diff': this.time_diff,
            'user': this.user,
            'pass': this.pass
        });
        return soap;
    };

    /* ------------------------------------------------------------------
    * Method: setAuth(user, pass)
    * ---------------------------------------------------------------- */
    setAuth(user, pass) {
        this.user = user || '';
        this.pass = pass || '';
        if (this.user) {
            this.oxaddr.auth = this.user + ':' + this.pass;
        } else {
            this.oxaddr.auth = '';
        }
    };

    /* ------------------------------------------------------------------
    * Method: getEventProperties([callback])
    * ---------------------------------------------------------------- */
    getEventProperties(callback) {
        let promise = new Promise((resolve, reject) => {
            let soap_body = '';
            soap_body += '<tev:GetEventProperties/>';
            let soap = this._createRequestSoap(soap_body);
            mOnvifSoap.requestCommand(this.oxaddr, 'GetEventProperties', soap).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (callback) {
            promise.then((result) => {
                callback(null, result);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };
}
/* ------------------------------------------------------------------
* node-onvif - http-auth.js
*
* Copyright (c) 2016, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2017-08-26
* ---------------------------------------------------------------- */

/* ------------------------------------------------------------------
* Constructor: OnvifHttpAuth()
* ---------------------------------------------------------------- */
class OnvifHttpAuth {
    constructor() {
        this.user = '';
        this.pass = '';
        this.method = '';
        this.path = '';
        this.nonce_count = 0;
        this.options = null;
    }
    /* ------------------------------------------------------------------
    * Method: request(options, callback)
    * ---------------------------------------------------------------- */
    request(options, callback) {
        this.options = JSON.parse(JSON.stringify(options))
        let http = (options && options.protocol === 'https:') ? mHttps : mHttp;
        if (options.auth && typeof (options.auth) === 'string') {
            let pair = options.auth.split(':');
            this.user = pair[0];
            this.pass = pair[1];
        }
        //delete options.auth;
        if (options.method && typeof (options.method) === 'string') {
            this.method = options.method.toUpperCase();
        } else {
            this.method = 'GET';
        }
        if (options.path && typeof (options.path) === 'string') {
            this.path = options.path;
        }
        let req = http.request(options, (res) => {
            if (res.statusCode === 401 && res.headers['www-authenticate']) {
                if (res.headers['www-authenticate'].match(/Digest realm/)) {
                    this._handleHttpDigest(http, res, callback);
                } else {
                    callback(res);
                }
            } else {
                callback(res);
            }
        });
        return req;
    };

    _handleHttpDigest(http, res, callback) {
        let o = this._parseAuthHeader(res.headers['www-authenticate']);
        if (!this.options.headers) {
            this.options.headers = {};
        }
        this.options.headers['Authorization'] = this._createAuthReqHeaderValue(o);
        let req = http.request(this.options, callback);
        req.end();
    };

    _createAuthReqHeaderValue(o) {
        let ha1 = this._createHash(o.algorithm, [this.user, o['Digest realm'], this.pass].join(':'));
        let ha2 = this._createHash(o.algorithm, [this.method, this.path].join(':'));
        let cnonce = this._createCnonce(8);
        this.nonce_count++;
        let nc = ('0000000' + this.nonce_count.toString(16)).slice(-8);
        let response = this._createHash(o.algorithm, [ha1, o.nonce, nc, cnonce, o.qop, ha2].join(':'));

        let hvalue = [
            'username="' + this.user + '"',
            'realm="' + o['Digest realm'] + '"',
            'nonce="' + o.nonce + '"',
            'uri="' + this.path + '"',
            'algorithm=' + o.algorithm,
            'qop=' + o.qop,
            'nc=' + nc,
            'cnonce="' + cnonce + '"',
            'response="' + response + '"'
        ].join(', ');
        hvalue = 'Digest ' + hvalue;
        return hvalue;
    };

    _createCnonce(digit) {
        let nonce = new Buffer(digit);
        for (let i = 0; i < digit; i++) {
            nonce.writeUInt8(Math.floor(Math.random() * 256), i);
        }
        return nonce.toString('hex');
    };

    _createHash(algo, data) {
        let hash = null;
        if (algo === 'MD5') {
            hash = mCrypto.createHash('md5');
        } else {
            hash = mCrypto.createHash('sha256');
        }
        hash.update(data, 'utf8');
        return hash.digest('hex');
    };

    _parseAuthHeader(h) {
        let o = {};
        h.split(/,\s*/).forEach((s) => {
            let pair = s.split('=');
            let k = pair[0];
            let v = pair[1];
            if (!k || !v) {
                return;
            }
            v = v.replace(/^\"/, '');
            v = v.replace(/\"$/, '');
            o[k] = v;
        });
        if (!o['algorithm']) { // workaround for DBPOWER
            o['algorithm'] = 'MD5';
        }
        return o;
    };

}

const mOnvifSoap = new OnvifSoap();
const mOnvifServiceDevice = OnvifServiceDevice;
const mOnvifServiceMedia = OnvifServiceMedia;
const mOnvifServicePtz = OnvifServicePtz;
const mOnvifServiceEvents = OnvifServiceEvents;
const mOnvifHttpAuth = OnvifHttpAuth;

/* ------------------------------------------------------------------
* Constructor: OnvifDevice(params)
* - params:
*    - address : IP address of the targeted device
*                (Required if the `xaddr` is not specified)
*    - xaddr   : URL of the entry point for the device management service
*                (Required if the `address' is not specified)
*                If the `xaddr` is specified, the `address` is ignored.
*    - user  : User name (Optional)
*    - pass  : Password (Optional)
* ---------------------------------------------------------------- */
class OnvifDevice {
    constructor(params) {
        if (!params || typeof (params) !== 'object') {
            throw new Error('The parameter was invalid.');
        }

        this.address = '';
        this.xaddr = '';
        this.user = '';
        this.pass = '';
        this.keepAddr = false;
        this.lastResponse = null; // for debug

        if (('xaddr' in params) && typeof (params['xaddr']) === 'string') {
            this.xaddr = params['xaddr'];
            let ourl = mUrl.parse(this.xaddr);
            this.address = ourl.hostname;
        } else if (('address' in params) && typeof (params['address']) === 'string') {
            this.keepAddr = true;
            this.address = params['address'];
            this.xaddr = 'http://' + this.address + '/onvif/device_service';
        } else {
            throw new Error('The parameter was invalid.');
        }
        if (('user' in params) && typeof (params['user']) === 'string') {
            this.user = params['user'] || '';
        }
        if (('pass' in params) && typeof (params['pass']) === 'string') {
            this.pass = params['pass'] || '';
        }

        this.oxaddr = mUrl.parse(this.xaddr);
        if (this.user) {
            this.oxaddr.auth = this.user + ':' + this.pass;
        }

        this.time_diff = 0;

        this.information = null;
        this.services = {
            'device': new mOnvifServiceDevice({ 'xaddr': this.xaddr, 'user': this.user, 'pass': this.pass }),
            'events': null,
            'imaging': null,
            'media': null,
            'ptz': null
        };
        this.profile_list = [];

        this.current_profile = null;
        this.ptz_moving = false;

        mEventEmitter.call(this);
    }

    _isValidCallback(callback) {
        return (callback && typeof (callback) === 'function') ? true : false;
    };

    _execCallback(callback, arg1, arg2) {
        if (this._isValidCallback(callback)) {
            callback(arg1, arg2);
        }
    };

    /* ------------------------------------------------------------------
    * Method: getInformation()
    * ---------------------------------------------------------------- */
    getInformation() {
        let o = this.information;
        if (o) {
            if (o instanceof Object) {
                o.rtsp = this.getRtspStreamUrl().replace('rtsp://', `rtsp://${this.user}:${this.pass}@`)
                o.resolution = this.getResolutionVideo()
            }
            return JSON.parse(JSON.stringify(o));
        } else {
            return null;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getCurrentProfile()
    * ---------------------------------------------------------------- */
    getCurrentProfile() {
        let o = this.current_profile;
        if (o) {
            return JSON.parse(JSON.stringify(o));
        } else {
            return null;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getProfileList()
    * ---------------------------------------------------------------- */
    getProfileList() {
        return JSON.parse(JSON.stringify(this.profile_list));
    };

    /* ------------------------------------------------------------------
    * Method: changeProfile(index|token)
    * ---------------------------------------------------------------- */
    changeProfile(index) {
        if (typeof (index) === 'number' && index >= 0 && index % 1 === 0) {
            let p = this.profile_list[index];
            if (p) {
                this.current_profile = p;
                return this.getCurrentProfile();
            } else {
                return null;
            }
        } else if (typeof (index) === 'string' && index.length > 0) {
            let new_profile = null;
            for (let i = 0; i < this.profile_list.length; i++) {
                if (this.profile_list[i]['token'] === index) {
                    new_profile = this.profile_list[i];
                    break;
                }
            }
            if (new_profile) {
                this.current_profile = new_profile;
                return this.getCurrentProfile();
            } else {
                return null;
            }
        } else {
            return null;
        }
    };

    /* ------------------------------------------------------------------
    * Method: getUdpStreamUrl()
    * ---------------------------------------------------------------- */
    getUdpStreamUrl() {
        if (!this.current_profile) {
            return '';
        }
        let url = this.current_profile['stream']['udp'];
        return url ? url : '';
    };

    /* ------------------------------------------------------------------
    * Method: getUdpStreamUrl()
    * ---------------------------------------------------------------- */
    getRtspStreamUrl() {
        if (!this.current_profile) {
            return '';
        }
        let url = this.current_profile['stream']['rtsp'];
        return url ? url : '';
    };

    /* ------------------------------------------------------------------
    * Method: getUdpStreamUrl()
    * ---------------------------------------------------------------- */
    getResolutionVideo() {
        if (!this.current_profile) {
            return '';
        }
        let url = this.current_profile['video']['encoder']['resolution'];
        return url ? url : '';
    };

    /* ------------------------------------------------------------------
    * Method: fetchSnapshot()
    * ---------------------------------------------------------------- */
    fetchSnapshot(callback) {
        let promise = new Promise((resolve, reject) => {
            if (!this.current_profile) {
                reject(new Error('No media profile is selected.'));
                return;
            }
            if (!this.current_profile['snapshot']) {
                reject(new Error('The device does not support snapshot or you have not authorized by the device.'));
                return;
            }
            let ourl = mUrl.parse(this.current_profile['snapshot']);
            let options = {
                protocol: ourl.protocol,
                auth: this.user + ':' + this.pass,
                hostname: ourl.hostname,
                port: ourl.port || 80,
                path: ourl.path,
                method: 'GET'
            };
            let req = new mOnvifHttpAuth().request(options, (res) => {
                let buffer_list = [];
                res.on('data', (buf) => {
                    buffer_list.push(buf);
                });
                res.on('end', () => {
                    if (res.statusCode === 200) {
                        let buffer = Buffer.concat(buffer_list);
                        let ct = res.headers['content-type'];
                        if (!ct) { // workaround for DBPOWER
                            ct = 'image/jpeg';
                        }
                        if (ct.match(/image\//)) {
                            resolve({ 'headers': res.headers, 'body': buffer });
                        } else if (ct.match(/^text\//)) {
                            reject(new Error(buffer.toString()));
                        } else {
                            reject(new Error('Unexpected data: ' + ct));
                        }
                    } else {
                        reject(new Error(res.statusCode + ' ' + res.statusMessage));
                    }
                });
                req.on('error', (error) => {
                    reject(error);
                });
            });
            req.on('error', (error) => {
                reject(error);
            });
            req.end();
        });
        if (this._isValidCallback(callback)) {
            promise.then((res) => {
                callback(null, res);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: ptzMove(params[, callback])
    * - params:
    *   - speed:
    *     - x     | Float   | required | speed for pan (in the range of -1.0 to 1.0)
    *     - y     | Float   | required | speed for tilt (in the range of -1.0 to 1.0)
    *     - z     | Float   | required | speed for zoom (in the range of -1.0 to 1.0)
    *   - timeout | Integer | optional | seconds (Default 1)
    * ---------------------------------------------------------------- */
    ptzMove(params, callback) {
        let promise = new Promise((resolve, reject) => {
            if (!this.current_profile) {
                reject(new Error('No media profile is selected.'));
                return;
            }
            if (!this.services['ptz']) {
                reject(new Error('The device does not support PTZ.'));
                return;
            }

            let speed = params['speed'];
            if (!speed) {
                speed = {};
            }
            let x = speed['x'] || 0;
            let y = speed['y'] || 0;
            let z = speed['z'] || 0;

            let timeout = params['timeout'];
            if (!timeout || typeof (timeout) !== 'number') {
                timeout = 1;
            }
            let p = {
                'ProfileToken': this.current_profile['token'],
                'Velocity': { 'x': x, 'y': y, 'z': z },
                'Timeout': timeout
            };
            this.ptz_moving = true;
            this.services['ptz'].continuousMove(p).then(() => {
                resolve();
            }).catch((error) => {
                reject(error);
            });
        });
        if (this._isValidCallback(callback)) {
            promise.then(() => {
                callback(null);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: ptzStop([callback])
    * ---------------------------------------------------------------- */
    ptzStop(callback) {
        let promise = new Promise((resolve, reject) => {
            if (!this.current_profile) {
                reject(new Error('No media profile is selected.'));
                return;
            }
            if (!this.services['ptz']) {
                reject(new Error('The device does not support PTZ.'));
                return;
            }
            this.ptz_moving = false;
            let p = {
                'ProfileToken': this.current_profile['token'],
                'PanTilt': true,
                'Zoom': true
            };
            this.services['ptz'].stop(p).then((result) => {
                resolve(result);
            }).catch((error) => {
                reject(error);
            });
        });
        if (this._isValidCallback(callback)) {
            promise.then((res) => {
                callback(null, res);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    /* ------------------------------------------------------------------
    * Method: setAuth(user, pass)
    * ---------------------------------------------------------------- */
    setAuth(user, pass) {
        this.user = user || '';
        this.pass = pass || '';
        if (this.user) {
            this.oxaddr.auth = this.user + ':' + this.pass;
        }
        for (let k in this.services) {
            let s = this.services[k];
            if (s) {
                this.services[k].setAuth(user, pass);
            }
        }
    };

    /* ------------------------------------------------------------------
    * Method: init([callback])
    * ---------------------------------------------------------------- */
    init(callback) {
        let promise = new Promise((resolve, reject) => {
            this._getSystemDateAndTime().then(() => {
                return this._getCapabilities();
            }).then(() => {
                return this._getDeviceInformation();
            }).then(() => {
                return this._mediaGetProfiles();
            }).then(() => {
                return this._mediaGetStreamURI();
            }).then(() => {
                return this._mediaGetSnapshotUri();
            }).then(() => {
                let info = { ...this.getInformation(), ...this.getProfileList() };
                resolve(info);
            }).catch((error) => {
                reject(error);
            });
        });
        if (this._isValidCallback(callback)) {
            promise.then((info) => {
                callback(null, info);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    // GetSystemDateAndTime (Access Class: PRE_AUTH)
    _getSystemDateAndTime() {
        let promise = new Promise((resolve, reject) => {
            this.services.device.getSystemDateAndTime((error, result) => {
                // Ignore the error becase some devices do not support
                // the GetSystemDateAndTime command and the error does
                // not cause any trouble.
                if (!error) {
                    this.time_diff = this.services.device.getTimeDiff();
                }
                resolve();
            });
        });
        return promise;
    };

    // GetCapabilities (Access Class: PRE_AUTH)
    _getCapabilities() {
        let promise = new Promise((resolve, reject) => {
            this.services.device.getCapabilities((error, result) => {
                this.lastResponse = result;
                if (error) {
                    reject(new Error('Failed to initialize the device: ' + error.toString()));
                    return;
                }
                let c = result['data']['GetCapabilitiesResponse']['Capabilities'];
                if (!c) {
                    reject(new Error('Failed to initialize the device: No capabilities were found.'));
                    return;
                }
                let events = c['Events'];
                if (events && events['XAddr']) {
                    this.services.events = new mOnvifServiceEvents({
                        'xaddr': this._getXaddr(events['XAddr']),
                        'time_diff': this.time_diff,
                        'user': this.user,
                        'pass': this.pass
                    });
                }
                let imaging = c['Imaging'];
                if (imaging && imaging['XAddr']) {
                    /*
                    this.services.imaging = new mOnvifServiceImaging({
                        'xaddr'    : imaging['XAddr'],
                        'time_diff': this.time_diff,
                        'user'     : this.user,
                        'pass'     : this.pass
                    });
                    */
                }
                let media = c['Media'];
                if (media && media['XAddr']) {
                    this.services.media = new mOnvifServiceMedia({
                        'xaddr': this._getXaddr(media['XAddr']),
                        'time_diff': this.time_diff,
                        'user': this.user,
                        'pass': this.pass
                    });
                }
                let ptz = c['PTZ'];
                if (ptz && ptz['XAddr']) {
                    this.services.ptz = new mOnvifServicePtz({
                        'xaddr': this._getXaddr(ptz['XAddr']),
                        'time_diff': this.time_diff,
                        'user': this.user,
                        'pass': this.pass
                    });
                }
                resolve();
            });
        });
        return promise;
    };

    // GetDeviceInformation (Access Class: READ_SYSTEM)
    _getDeviceInformation() {
        let promise = new Promise((resolve, reject) => {
            this.services.device.getDeviceInformation((error, result) => {
                if (error) {
                    reject(new Error('Failed to initialize the device: ' + error.toString()));
                } else {
                    this.information = result['data']['GetDeviceInformationResponse'];
                    resolve();
                }
            });
        });
        return promise;
    };

    // Media::GetProfiles (Access Class: READ_MEDIA)
    _mediaGetProfiles() {
        let promise = new Promise((resolve, reject) => {
            this.services.media.getProfiles((error, result) => {
                this.lastResponse = result;
                if (error) {
                    reject(new Error('Failed to initialize the device: ' + error.toString()));
                    return;
                }
                let profiles = result['data']['GetProfilesResponse']['Profiles'];
                if (!profiles) {
                    reject(new Error('Failed to initialize the device: The targeted device does not any media profiles.'));
                    return;
                }
                profiles = [].concat(profiles) // in case profiles is not a list, then forEach below will report an error
                profiles.forEach((p) => {
                    let profile = {
                        'token': p['$']['token'],
                        'name': p['Name'],
                        'snapshot': '',
                        'stream': {
                            'udp': '',
                            'http': '',
                            'rtsp': ''
                        },
                        'video': {
                            'source': null,
                            'encoder': null
                        },
                        'audio': {
                            'source': null,
                            'encoder': null
                        },
                        'ptz': {
                            'range': {
                                'x': {
                                    'min': 0,
                                    'max': 0
                                },
                                'y': {
                                    'min': 0,
                                    'max': 0
                                },
                                'z': {
                                    'min': 0,
                                    'max': 0
                                }
                            }
                        }
                    };

                    if (p['VideoSourceConfiguration']) {
                        profile['video']['source'] = {
                            'token': p['VideoSourceConfiguration']['$']['token'],
                            'name': p['VideoSourceConfiguration']['Name'],
                            'bounds': {
                                'width': parseInt(p['VideoSourceConfiguration']['Bounds']['$']['width'], 10),
                                'height': parseInt(p['VideoSourceConfiguration']['Bounds']['$']['height'], 10),
                                'x': parseInt(p['VideoSourceConfiguration']['Bounds']['$']['x'], 10),
                                'y': parseInt(p['VideoSourceConfiguration']['Bounds']['$']['y'], 10)
                            }
                        };
                    }
                    if (p['VideoEncoderConfiguration']) {
                        profile['video']['encoder'] = {
                            'token': p['VideoEncoderConfiguration']['$']['token'],
                            'name': p['VideoEncoderConfiguration']['Name'],
                            'resolution': {
                                'width': parseInt(p?.VideoEncoderConfiguration?.Resolution?.Width || 0, 10),
                                'height': parseInt(p?.VideoEncoderConfiguration?.Resolution?.Height || 0, 10),
                            },
                            'quality': parseInt(p['VideoEncoderConfiguration']['Quality'], 10),
                            'framerate': parseInt(p?.VideoEncoderConfiguration?.RateControl?.FrameRateLimit || 0, 10),
                            'bitrate': parseInt(p?.VideoEncoderConfiguration?.RateControl?.BitrateLimit || 0, 10),
                            'encoding': p?.VideoEncoderConfiguration?.Encoding,
                        };
                    }
                    if (p['AudioSourceConfiguration']) {
                        profile['audio']['source'] = {
                            'token': p['AudioSourceConfiguration']['$']['token'],
                            'name': p['AudioSourceConfiguration']['Name']
                        };
                    }
                    if (p['AudioEncoderConfiguration']) {
                        profile['audio']['encoder'] = {
                            'token': ('$' in p['AudioEncoderConfiguration']) ? p['AudioEncoderConfiguration']['$']['token'] : '',
                            'name': p['AudioEncoderConfiguration']['Name'],
                            'bitrate': parseInt(p['AudioEncoderConfiguration']['Bitrate'], 10),
                            'samplerate': parseInt(p['AudioEncoderConfiguration']['SampleRate'], 10),
                            'encoding': p['AudioEncoderConfiguration']['Encoding']
                        };
                    }
                    if (p['PTZConfiguration']) {
                        try {
                            let r = p['PTZConfiguration']['PanTiltLimits']['Range'];
                            let xr = r['XRange'];
                            let x = profile['ptz']['range']['x'];
                            x['min'] = parseFloat(xr['Min']);
                            x['max'] = parseFloat(xr['Max']);
                        } catch (e) { }
                        try {
                            let r = p['PTZConfiguration']['PanTiltLimits']['Range'];
                            let yr = r['YRange'];
                            let y = profile['ptz']['range']['y'];
                            y['min'] = parseFloat(yr['Min']);
                            y['max'] = parseFloat(yr['Max']);
                        } catch (e) { }
                        try {
                            let r = p['PTZConfiguration']['ZoomLimits']['Range'];
                            let zr = r['XRange'];
                            let z = profile['ptz']['range']['z'];
                            z['min'] = parseFloat(zr['Min']);
                            z['max'] = parseFloat(zr['Max']);
                        } catch (e) { }
                    }
                    if (this.profile_list.find(item => {
                        return item.token == profile.token;
                    }) === undefined) {
                        this.profile_list.push(profile);
                    }
                    if (!this.current_profile) {
                        this.current_profile = profile;
                    }
                });
                resolve();
            });
        });
        return promise;
    };

    // Media::GetStreamURI (Access Class: READ_MEDIA)
    _mediaGetStreamURI() {
        let protocol_list = ['UDP', 'HTTP', 'RTSP'];
        let promise = new Promise((resolve, reject) => {
            let profile_index = 0;
            let protocol_index = 0;
            let getStreamUri = () => {
                let profile = this.profile_list[profile_index];
                if (profile) {
                    let protocol = protocol_list[protocol_index];
                    if (protocol) {
                        let token = profile['token'];
                        let params = {
                            'ProfileToken': token,
                            'Protocol': protocol
                        };
                        this.services.media.getStreamUri(params, (error, result) => {
                            this.lastResponse = result;
                            if (!error) {
                                let uri = result['data']['GetStreamUriResponse']['MediaUri']['Uri'];
                                uri = this._getUri(uri);
                                this.profile_list[profile_index]['stream'][protocol.toLowerCase()] = uri;
                            }
                            protocol_index++;
                            getStreamUri();
                        });
                    } else {
                        profile_index++;
                        protocol_index = 0;
                        getStreamUri();
                    }
                } else {
                    resolve();
                    return;
                }
            };
            getStreamUri();
        });
        return promise;
    };

    // Media::GetSnapshotUri (Access Class: READ_MEDIA)
    _mediaGetSnapshotUri() {
        let promise = new Promise((resolve, reject) => {
            let profile_index = 0;
            let getSnapshotUri = () => {
                let profile = this.profile_list[profile_index];
                if (profile) {
                    let params = { 'ProfileToken': profile['token'] };
                    this.services.media.getSnapshotUri(params, (error, result) => {
                        this.lastResponse = result;
                        if (!error) {
                            try {
                                let snapshotUri = result['data']['GetSnapshotUriResponse']['MediaUri']['Uri'];
                                snapshotUri = this._getSnapshotUri(snapshotUri);
                                profile['snapshot'] = snapshotUri;
                            } catch (e) { log('getSnapshotUri', e, 'error'); }
                        }
                        profile_index++;
                        getSnapshotUri();
                    });
                } else {
                    resolve();
                }
            };
            getSnapshotUri();
        });
        return promise;
    };

    _getXaddr(directXaddr) {
        if (!this.keepAddr) return directXaddr;
        const path = mUrl.parse(directXaddr).path;
        return 'http://' + this.address + path;
    }

    _getUri(directUri) {
        if (typeof (directUri) === 'object' && directUri['_']) {
            directUri = directUri['_'];
        }
        if (!this.keepAddr) return directUri;
        const base = mUrl.parse('http://' + this.address);
        const parts = mUrl.parse(directUri);
        const newParts = {
            host: base.host,
            pathname: base.pathname + parts.pathname
        };
        const newUri = mUrl.format(newParts);
        return newUri;
    }

    _getSnapshotUri(directUri) {
        if (typeof (directUri) === 'object' && directUri['_']) {
            directUri = directUri['_'];
        }
        if (!this.keepAddr) return directUri;
        const base = mUrl.parse('http://' + this.address);
        const parts = mUrl.parse(directUri);
        const newParts = {
            protocol: parts.protocol,
            host: base.host,
            pathname: base.pathname + parts.pathname
        };
        const newUri = mUrl.format(newParts);
        return newUri;
    }

};
mUtil.inherits(OnvifDevice, mEventEmitter);
/* ------------------------------------------------------------------
* node-onvif - soap.js
*
* Copyright (c) 2016-2018, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2018-08-13
* ---------------------------------------------------------------- */

/* ------------------------------------------------------------------
* node-onvif - node-onvif.js
*
* Copyright (c) 2016 - 2017, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2017-09-30
* ---------------------------------------------------------------- */

/* ------------------------------------------------------------------
* Constructor: Onvif()
* ---------------------------------------------------------------- */
class Onvif {
    constructor() {
        // Public
        this.OnvifDevice = OnvifDevice;
        // Private
        this._OnvifSoap = mOnvifSoap;
        this._MULTICAST_ADDRESS = '239.255.255.250';
        this._udp = null;
        this._devices = {};
        this._DISCOVERY_INTERVAL = 150; // ms
        this._DISCOVERY_RETRY_MAX = 3;
        this._DISCOVERY_WAIT = 1500; // ms
        this._discovery_interval_timer = null;
        this._discovery_wait_timer = null;
    }

    /* ------------------------------------------------------------------
    * Method: startDiscovery(callback)
    * [Caution]
    *   This method has been depricated.
    *   Use the startProbe() method instead of this method.
    * ---------------------------------------------------------------- */
    startDiscovery(callback) {
        this.startProbe().then((list) => {
            let execCallback = () => {
                let d = list.shift();
                if (d) {
                    callback(d);
                    setTimeout(() => {
                        execCallback();
                    }, 100);
                }
            }
            execCallback();
        }).catch((error) => {
            callback(error);
        });
    };

    /* ------------------------------------------------------------------
    * Method: startProbe([callback])
    * ---------------------------------------------------------------- */
    startProbe(callback) {
        let promise = new Promise((resolve, reject) => {
            this._devices = {};
            this._udp = mDgram.createSocket('udp4');
            this._udp.once('error', (error) => {
                reject(error);
            });
            this._udp.on('message', (buf, device_info) => {
                this._OnvifSoap.parse(buf.toString()).then(data => {
                    let type = '';
                    let urn = '';
                    let xaddrs = [];
                    let scopes = [];
                    let types = '';
                    let name = '';
                    let hardware = '';
                    let location = '';
                    let result = {};
                    Object.keys(data).forEach(key => {
                        let value = data[key].replace(/\n/gi, '').trim();
                        result[key] = value;
                    })
                    let resultString = JSON.stringify(result)
                    if (resultString.includes('ProbeMatches')) {
                        try {
                            let probe_matches = result['Body']['ProbeMatches']
                            // make sure the right data exists
                            if (probe_matches !== undefined) {
                                let probe_match = probe_matches['ProbeMatch'];
                                urn = probe_match['EndpointReference']['Address'];
                                xaddrs = probe_match['XAddrs'].split(/\s+/);
                                if (typeof (probe_match['Scopes']) === 'string') {
                                    scopes = probe_match['Scopes'].split(/\s+/);
                                } else if (typeof (probe_match['Scopes']) === 'object' && typeof (probe_match['Scopes']['_']) === 'string') {
                                    scopes = probe_match['Scopes']['_'].split(/\s+/);
                                }
                                // modified to support Pelco cameras
                                if (typeof (probe_match['Types']) === 'string') {
                                    types = probe_match['Types'].split(/\s+/);
                                } else if (typeof (probe_match['Types']) === 'object' && typeof (probe_match['Types']['_']) === 'string') {
                                    types = probe_match['Types']['_'].split(/\s+/)
                                }
                            }
                        } catch (e) {
                            return;
                        };
                        if (urn && xaddrs.length > 0 && scopes.length > 0) {
                            if (!this._devices[urn]) {
                                scopes.forEach((s) => {
                                    if (s.indexOf('onvif://www.onvif.org/hardware/') === 0) {
                                        hardware = s.split('/').pop();
                                    } else if (s.indexOf('onvif://www.onvif.org/location/') === 0) {
                                        location = s.split('/').pop();
                                    } else if (s.indexOf('onvif://www.onvif.org/name/') === 0) {
                                        name = s.split('/').pop();
                                        name = name.replace(/_/g, ' ');
                                    }
                                });
                            }
                        }
                    } else {
                        // 海康设备
                        type = 'hikvision';
                        types = result['Types'];
                        urn = result['IPv4Address'];
                        name = result['DeviceDescription'];
                        if (result['HttpPort']) {
                            xaddrs = [`http://${urn}${result['HttpPort'] == '80' ? '' : `:${result['HttpPort']}`}/onvif/device_service`];
                        }
                    }
                    let probe = {
                        'urn': urn,
                        'name': name,
                        'hardware': hardware,
                        'location': location,
                        'type': type,
                        'types': types,
                        'xaddrs': xaddrs,
                        'scopes': scopes
                    };
                    this._devices[urn] = probe;
                }).catch((error) => {
                    // Do nothing.
                });
            });

            this._udp.bind(() => {
                this._udp.removeAllListeners('error');
                this._sendProbe().then(() => {
                    // Do nothing.
                }).catch((error) => {
                    reject(error);
                });
                this._discovery_wait_timer = setTimeout(() => {
                    this.stopProbe().then(() => {
                        let device_list = [];
                        Object.keys(this._devices).forEach((urn) => {
                            device_list.push(this._devices[urn]);
                        });
                        resolve(device_list);
                    }).catch((error) => {
                        reject(error);
                    });
                }, this._DISCOVERY_WAIT);
            });
        });

        if (this._isValidCallback(callback)) {
            promise.then((device_list) => {
                callback(null, device_list);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };

    _isValidCallback(callback) {
        return (callback && typeof (callback) === 'function') ? true : false;
    };

    _execCallback(callback, arg1, arg2) {
        if (this._isValidCallback(callback)) {
            callback(arg1, arg2);
        }
    };

    _sendProbe(callback) {
        let soap_set = [];
        let soap_tmpl = '';
        soap_tmpl = `<?xml version="1.0" encoding="utf-8"?>
<Probe>
    <Uuid>
        __uuid__
    </Uuid>
    <Types>__type__</Types>
</Probe>`;
        ['inquiry'].forEach(type => {
            let s = soap_tmpl;
            s = s.replace('__type__', type);
            s = s.replace('__uuid__', this._createUuidV4());
            soap_set.push({
                tmpl: s,
                port: 37020
            });
        });
        soap_tmpl = ''
        soap_tmpl += '<?xml version="1.0" encoding="UTF-8"?>';
        soap_tmpl += '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">';
        soap_tmpl += '  <s:Header>';
        soap_tmpl += '    <a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>';
        soap_tmpl += '    <a:MessageID>uuid:__uuid__</a:MessageID>';
        soap_tmpl += '    <a:ReplyTo>';
        soap_tmpl += '      <a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>';
        soap_tmpl += '    </a:ReplyTo>';
        soap_tmpl += '    <a:To s:mustUnderstand="1">urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>';
        soap_tmpl += '  </s:Header>';
        soap_tmpl += '  <s:Body>';
        soap_tmpl += '    <Probe xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery">';
        soap_tmpl += '      <d:Types xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:dp0="http://www.onvif.org/ver10/network/wsdl">dp0:__type__</d:Types>';
        soap_tmpl += '    </Probe>';
        soap_tmpl += '  </s:Body>';
        soap_tmpl += '</s:Envelope>';
        soap_tmpl = soap_tmpl.replace(/\>\s+\</g, '><');
        soap_tmpl = soap_tmpl.replace(/\s+/, ' ');

        ['NetworkVideoTransmitter', 'Device', 'NetworkVideoDisplay'].forEach((type) => {
            let s = soap_tmpl;
            s = s.replace('__type__', type);
            s = s.replace('__uuid__', this._createUuidV4());
            soap_set.push({
                tmpl: s,
                port: 3702
            });
        });
        // console.log(soap_set)
        let soap_list = [];
        for (let i = 0; i < this._DISCOVERY_RETRY_MAX; i++) {
            soap_set.forEach((s) => {
                soap_list.push(s);
            });
        }
        let promise = new Promise((resolve, reject) => {
            if (!this._udp) {
                reject(new Error('No UDP connection is available. The init() method might not be called yet.'));
            }
            let send = () => {
                let soap = soap_list.shift();
                if (soap) {
                    let buf = Buffer(soap['tmpl'], 'utf8');
                    this._udp.send(buf, 0, buf.length, soap['port'], this._MULTICAST_ADDRESS, (error, bytes) => {
                        this._discovery_interval_timer = setTimeout(() => {
                            send();
                        }, this._DISCOVERY_INTERVAL);
                    });
                } else {
                    resolve();
                }
            };
            send();
        });
        return promise;
    };

    _createUuidV4() {
        let clist = mCrypto.randomBytes(16).toString('hex').toLowerCase().split('');
        clist[12] = '4';
        clist[16] = (parseInt(clist[16], 16) & 3 | 8).toString(16);
        let m = clist.join('').match(/^(.{8})(.{4})(.{4})(.{4})(.{12})/);
        let uuid = [m[1], m[2], m[3], m[4], m[5]].join('-');
        this._uuid = uuid;
        return uuid;
    };

    /* ------------------------------------------------------------------
    * Method: stopDiscovery([callback])
    * [Caution]
    *   This method has been depricated.
    *   Use the stopProbe() method instead of this method.
    * ---------------------------------------------------------------- */
    stopDiscovery(callback) {
        this.stopProbe().then(() => {
            this._execCallback(callback);
        }).catch((error) => {
            this._execCallback(callback, error);
        });
    };

    /* ------------------------------------------------------------------
    * Method: stopProbe([callback])
    * ---------------------------------------------------------------- */
    stopProbe(callback) {
        if (this._discovery_interval_timer !== null) {
            clearTimeout(this._discovery_interval_timer);
            this._discovery_interval_timer = null;
        }
        if (this._discovery_wait_timer !== null) {
            clearTimeout(this._discovery_wait_timer);
            this._discovery_wait_timer = null;
        }

        let promise = new Promise((resolve, reject) => {
            if (this._udp) {
                this._udp.close(() => {
                    this._udp.unref()
                    this._udp = null;
                    resolve();
                });
            } else {
                resolve();
            }
        });

        if (this._isValidCallback(callback)) {
            promise.then(() => {
                callback(null);
            }).catch((error) => {
                callback(error);
            });
        } else {
            return promise;
        }
    };
}
module.exports = { VideoStream, Onvif };